diff --git a/.github/workflows/aws_publisher.yaml b/.github/workflows/aws_publisher.yaml index 0d0f081f56..c7b80c54f9 100644 --- a/.github/workflows/aws_publisher.yaml +++ b/.github/workflows/aws_publisher.yaml @@ -31,7 +31,7 @@ jobs: echo "Packer will be triggered in this dir $WORK_DIR" - name: Configure AWS credentials for Kafka-UI account - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v1-node16 with: aws-access-key-id: ${{ secrets.AWS_AMI_PUBLISH_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_AMI_PUBLISH_KEY_SECRET }} diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 17a2fc1007..dc4700458f 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -1,4 +1,4 @@ -name: backend +name: Backend build and test on: push: branches: diff --git a/.github/workflows/block_merge.yml b/.github/workflows/block_merge.yml index 28f5cde3f4..e1cdb3ac8e 100644 --- a/.github/workflows/block_merge.yml +++ b/.github/workflows/block_merge.yml @@ -6,7 +6,7 @@ jobs: block_merge: runs-on: ubuntu-latest steps: - - uses: mheap/github-action-required-labels@v2 + - uses: mheap/github-action-required-labels@v3 with: mode: exactly count: 0 diff --git a/.github/workflows/branch-deploy.yml b/.github/workflows/branch-deploy.yml index 0cd0ebb893..1cc5fee39b 100644 --- a/.github/workflows/branch-deploy.yml +++ b/.github/workflows/branch-deploy.yml @@ -1,4 +1,4 @@ -name: DeployFromBranch +name: Feature testing init on: workflow_dispatch: @@ -10,6 +10,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: get branch name id: extract_branch run: | @@ -43,7 +45,7 @@ jobs: restore-keys: | ${{ runner.os }}-buildx- - name: Configure AWS credentials for Kafka-UI account - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v1-node16 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/branch-remove.yml b/.github/workflows/branch-remove.yml index 59024af6cb..c93fa89eba 100644 --- a/.github/workflows/branch-remove.yml +++ b/.github/workflows/branch-remove.yml @@ -1,4 +1,4 @@ -name: RemoveCustomDeployment +name: Feature testing destroy on: workflow_dispatch: pull_request: diff --git a/.github/workflows/build-public-image.yml b/.github/workflows/build-public-image.yml index 9db111c7a8..c79996d2ac 100644 --- a/.github/workflows/build-public-image.yml +++ b/.github/workflows/build-public-image.yml @@ -9,6 +9,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: get branch name id: extract_branch run: | @@ -40,7 +42,7 @@ jobs: restore-keys: | ${{ runner.os }}-buildx- - name: Configure AWS credentials for Kafka-UI account - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v1-node16 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/create-branch-for-helm.yaml b/.github/workflows/create-branch-for-helm.yaml index b3e7d5f4ed..d755e3da8f 100644 --- a/.github/workflows/create-branch-for-helm.yaml +++ b/.github/workflows/create-branch-for-helm.yaml @@ -1,4 +1,4 @@ -name: prepare-helm-release +name: Prepare helm release on: repository_dispatch: types: [prepare-helm-release] diff --git a/.github/workflows/cve.yaml b/.github/workflows/cve.yaml index ef7d784ca1..5b0e2779dd 100644 --- a/.github/workflows/cve.yaml +++ b/.github/workflows/cve.yaml @@ -55,7 +55,7 @@ jobs: cache-to: type=local,dest=/tmp/.buildx-cache - name: Run CVE checks - uses: aquasecurity/trivy-action@0.8.0 + uses: aquasecurity/trivy-action@0.9.1 with: image-ref: "provectuslabs/kafka-ui:${{ steps.build.outputs.version }}" format: "table" diff --git a/.github/workflows/delete-public-image.yml b/.github/workflows/delete-public-image.yml index 47e08713d8..56c795d0b5 100644 --- a/.github/workflows/delete-public-image.yml +++ b/.github/workflows/delete-public-image.yml @@ -15,7 +15,7 @@ jobs: tag='${{ github.event.pull_request.number }}' echo "tag=${tag}" >> $GITHUB_OUTPUT - name: Configure AWS credentials for Kafka-UI account - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v1-node16 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index 5fef435fc9..70fff483da 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -1,4 +1,4 @@ -name: Documentation +name: Documentation URLs linter on: pull_request: types: diff --git a/.github/workflows/e2e-automation.yml b/.github/workflows/e2e-automation.yml new file mode 100644 index 0000000000..5a5018c9e5 --- /dev/null +++ b/.github/workflows/e2e-automation.yml @@ -0,0 +1,83 @@ +name: E2E Automation suite +on: + workflow_dispatch: + inputs: + test_suite: + description: 'Select test suite to run' + default: 'regression' + required: true + type: choice + options: + - regression + - sanity + - smoke + qase_token: + description: 'Set Qase token to enable integration' + required: false + type: string + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.sha }} + - name: Set up environment + id: set_env_values + run: | + cat "./kafka-ui-e2e-checks/.env.ci" >> "./kafka-ui-e2e-checks/.env" + - name: Pull with Docker + id: pull_chrome + run: | + docker pull selenium/standalone-chrome:103.0 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'zulu' + cache: 'maven' + - name: Build with Maven + id: build_app + run: | + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }} + ./mvnw -B -V -ntp clean install -Pprod -Dmaven.test.skip=true ${{ github.event.inputs.extraMavenOptions }} + - name: Compose with Docker + id: compose_app + # use the following command until #819 will be fixed + run: | + docker-compose -f ./documentation/compose/e2e-tests.yaml up -d + - name: Run test suite + run: | + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }} + ./mvnw -B -V -ntp -DQASEIO_API_TOKEN=${{ github.event.inputs.qase_token }} -Dsurefire.suiteXmlFiles='src/test/resources/${{ github.event.inputs.test_suite }}.xml' -Dsuite=${{ github.event.inputs.test_suite }} -f 'kafka-ui-e2e-checks' test -Pprod + - name: Generate Allure report + uses: simple-elf/allure-report-action@master + if: always() + id: allure-report + with: + allure_results: ./kafka-ui-e2e-checks/allure-results + gh_pages: allure-results + allure_report: allure-report + subfolder: allure-results + report_url: "http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com" + - uses: jakejarvis/s3-sync-action@master + if: always() + env: + AWS_S3_BUCKET: 'kafkaui-allure-reports' + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: 'eu-central-1' + SOURCE_DIR: 'allure-history/allure-results' + - name: Deploy report to Amazon S3 + if: always() + uses: Sibz/github-status-action@v1.1.6 + with: + authToken: ${{secrets.GITHUB_TOKEN}} + context: "Test report" + state: "success" + sha: ${{ github.sha }} + target_url: http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com/${{ github.run_number }} + - name: Dump Docker logs on failure + if: failure() + uses: jwalton/gh-docker-logs@v2.2.1 diff --git a/.github/workflows/e2e-checks.yaml b/.github/workflows/e2e-checks.yaml index 7725d33d4a..962a134684 100644 --- a/.github/workflows/e2e-checks.yaml +++ b/.github/workflows/e2e-checks.yaml @@ -1,7 +1,7 @@ -name: e2e-checks +name: E2E PR health check on: pull_request_target: - types: ["opened", "edited", "reopened", "synchronize"] + types: [ "opened", "edited", "reopened", "synchronize" ] paths: - "kafka-ui-api/**" - "kafka-ui-contract/**" @@ -15,6 +15,12 @@ jobs: - uses: actions/checkout@v3 with: ref: ${{ github.event.pull_request.head.sha }} + - name: Configure AWS credentials for Kafka-UI account + uses: aws-actions/configure-aws-credentials@v1-node16 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-central-1 - name: Set the values id: set_env_values run: | @@ -33,7 +39,7 @@ jobs: id: build_app run: | ./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.pull_request.head.sha }} - ./mvnw -B -V -ntp clean package -Pprod -Dmaven.test.skip=true ${{ github.event.inputs.extraMavenOptions }} + ./mvnw -B -V -ntp clean install -Pprod -Dmaven.test.skip=true ${{ github.event.inputs.extraMavenOptions }} - name: compose app id: compose_app # use the following command until #819 will be fixed @@ -42,7 +48,7 @@ jobs: - name: e2e run run: | ./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.pull_request.head.sha }} - ./mvnw -B -V -ntp -DQASEIO_API_TOKEN=${{ secrets.QASEIO_API_TOKEN }} -pl '!kafka-ui-api' test -Pprod + ./mvnw -B -V -ntp -Dsurefire.suiteXmlFiles='src/test/resources/smoke.xml' -f 'kafka-ui-e2e-checks' test -Pprod - name: Generate allure report uses: simple-elf/allure-report-action@master if: always() @@ -57,8 +63,6 @@ jobs: if: always() env: AWS_S3_BUCKET: 'kafkaui-allure-reports' - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_REGION: 'eu-central-1' SOURCE_DIR: 'allure-history/allure-results' - name: Post the link to allure report @@ -66,7 +70,7 @@ jobs: uses: Sibz/github-status-action@v1.1.6 with: authToken: ${{secrets.GITHUB_TOKEN}} - context: "Test report" + context: "Click Details button to open Allure report" state: "success" sha: ${{ github.event.pull_request.head.sha || github.sha }} target_url: http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com/${{ github.run_number }} diff --git a/.github/workflows/e2e-manual.yml b/.github/workflows/e2e-manual.yml new file mode 100644 index 0000000000..68963e29fa --- /dev/null +++ b/.github/workflows/e2e-manual.yml @@ -0,0 +1,43 @@ +name: E2E Manual suite +on: + workflow_dispatch: + inputs: + test_suite: + description: 'Select test suite to run' + default: 'manual' + required: true + type: choice + options: + - manual + - qase + qase_token: + description: 'Set Qase token to enable integration' + required: true + type: string + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.sha }} + - name: Set up environment + id: set_env_values + run: | + cat "./kafka-ui-e2e-checks/.env.ci" >> "./kafka-ui-e2e-checks/.env" + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'zulu' + cache: 'maven' + - name: Build with Maven + id: build_app + run: | + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }} + ./mvnw -B -V -ntp clean install -Pprod -Dmaven.test.skip=true ${{ github.event.inputs.extraMavenOptions }} + - name: Run test suite + run: | + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }} + ./mvnw -B -V -ntp -DQASEIO_API_TOKEN=${{ github.event.inputs.qase_token }} -Dsurefire.suiteXmlFiles='src/test/resources/${{ github.event.inputs.test_suite }}.xml' -Dsuite=${{ github.event.inputs.test_suite }} -f 'kafka-ui-e2e-checks' test -Pprod diff --git a/.github/workflows/e2e-weekly.yml b/.github/workflows/e2e-weekly.yml new file mode 100644 index 0000000000..2bf2001ec2 --- /dev/null +++ b/.github/workflows/e2e-weekly.yml @@ -0,0 +1,70 @@ +name: E2E Weekly suite +on: + schedule: + - cron: '0 1 * * 1' + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.sha }} + - name: Set up environment + id: set_env_values + run: | + cat "./kafka-ui-e2e-checks/.env.ci" >> "./kafka-ui-e2e-checks/.env" + - name: Pull with Docker + id: pull_chrome + run: | + docker pull selenium/standalone-chrome:103.0 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'zulu' + cache: 'maven' + - name: Build with Maven + id: build_app + run: | + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }} + ./mvnw -B -V -ntp clean install -Pprod -Dmaven.test.skip=true ${{ github.event.inputs.extraMavenOptions }} + - name: Compose with Docker + id: compose_app + # use the following command until #819 will be fixed + run: | + docker-compose -f ./documentation/compose/e2e-tests.yaml up -d + - name: Run test suite + run: | + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }} + ./mvnw -B -V -ntp -DQASEIO_API_TOKEN=${{ secrets.QASEIO_API_TOKEN }} -Dsurefire.suiteXmlFiles='src/test/resources/sanity.xml' -Dsuite=weekly -f 'kafka-ui-e2e-checks' test -Pprod + - name: Generate Allure report + uses: simple-elf/allure-report-action@master + if: always() + id: allure-report + with: + allure_results: ./kafka-ui-e2e-checks/allure-results + gh_pages: allure-results + allure_report: allure-report + subfolder: allure-results + report_url: "http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com" + - uses: jakejarvis/s3-sync-action@master + if: always() + env: + AWS_S3_BUCKET: 'kafkaui-allure-reports' + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: 'eu-central-1' + SOURCE_DIR: 'allure-history/allure-results' + - name: Deploy report to Amazon S3 + if: always() + uses: Sibz/github-status-action@v1.1.6 + with: + authToken: ${{secrets.GITHUB_TOKEN}} + context: "Test report" + state: "success" + sha: ${{ github.sha }} + target_url: http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com/${{ github.run_number }} + - name: Dump Docker logs on failure + if: failure() + uses: jwalton/gh-docker-logs@v2.2.1 diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml index baa2551d1c..05ef18e383 100644 --- a/.github/workflows/frontend.yaml +++ b/.github/workflows/frontend.yaml @@ -1,4 +1,4 @@ -name: frontend +name: Frontend build and test on: push: branches: @@ -24,7 +24,7 @@ jobs: with: version: 7.4.0 - name: Install node - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: "16.15.0" cache: "pnpm" diff --git a/.github/workflows/helm.yaml b/.github/workflows/helm.yaml index b8c88a4305..427c63e0b3 100644 --- a/.github/workflows/helm.yaml +++ b/.github/workflows/helm.yaml @@ -1,4 +1,4 @@ -name: Helm +name: Helm linter on: pull_request: types: ["opened", "edited", "reopened", "synchronize"] diff --git a/.github/workflows/master.yaml b/.github/workflows/master.yaml index 4d1173958b..01651ee76d 100644 --- a/.github/workflows/master.yaml +++ b/.github/workflows/master.yaml @@ -1,4 +1,4 @@ -name: Master +name: Master branch build & deploy on: workflow_dispatch: push: @@ -9,6 +9,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up JDK uses: actions/setup-java@v3 diff --git a/.github/workflows/pr-checks.yaml b/.github/workflows/pr-checks.yaml index 74ff75b833..80f4e6eddc 100644 --- a/.github/workflows/pr-checks.yaml +++ b/.github/workflows/pr-checks.yaml @@ -7,7 +7,7 @@ jobs: task-check: runs-on: ubuntu-latest steps: - - uses: kentaro-m/task-completed-checker-action@v0.1.0 + - uses: kentaro-m/task-completed-checker-action@v0.1.1 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" - uses: dekinderfiets/pr-description-enforcer@0.0.1 diff --git a/.github/workflows/release-serde-api.yaml b/.github/workflows/release-serde-api.yaml index 227dbcec24..e9a35ee30e 100644 --- a/.github/workflows/release-serde-api.yaml +++ b/.github/workflows/release-serde-api.yaml @@ -1,4 +1,4 @@ -name: Release-serde-api +name: Release serde api on: workflow_dispatch jobs: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0e87c10fd2..1990fbdbc5 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -12,6 +12,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} - run: | git config user.name github-actions @@ -33,7 +34,7 @@ jobs: echo "version=${VERSION}" >> $GITHUB_OUTPUT - name: Upload files to a GitHub release - uses: svenstaro/upload-release-action@2.3.0 + uses: svenstaro/upload-release-action@2.5.0 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: kafka-ui-api/target/kafka-ui-api-${{ steps.build.outputs.version }}.jar diff --git a/.github/workflows/separate_env_public_create.yml b/.github/workflows/separate_env_public_create.yml index 551001031c..e10b1a382c 100644 --- a/.github/workflows/separate_env_public_create.yml +++ b/.github/workflows/separate_env_public_create.yml @@ -12,6 +12,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: get branch name id: extract_branch run: | @@ -45,7 +47,7 @@ jobs: restore-keys: | ${{ runner.os }}-buildx- - name: Configure AWS credentials for Kafka-UI account - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v1-node16 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index aafb50ceda..0a5d2e064c 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v6 + - uses: actions/stale@v7 with: days-before-issue-stale: 7 days-before-issue-close: 3 diff --git a/.github/workflows/terraform-deploy.yml b/.github/workflows/terraform-deploy.yml index 8ec07be0a9..6df90e7338 100644 --- a/.github/workflows/terraform-deploy.yml +++ b/.github/workflows/terraform-deploy.yml @@ -1,4 +1,4 @@ -name: terraform_deploy +name: Terraform deploy on: workflow_dispatch: inputs: diff --git a/README.md b/README.md index a0ca023bc0..e924621536 100644 --- a/README.md +++ b/README.md @@ -185,32 +185,30 @@ For example, if you want to use an environment variable to set the `name` parame |`KAFKA_CLUSTERS_0_KSQLDBSERVERAUTH_PASSWORD` | KSQL DB server's basic authentication password |`KAFKA_CLUSTERS_0_KSQLDBSERVERSSL_KEYSTORELOCATION` |Path to the JKS keystore to communicate to KSQL DB |`KAFKA_CLUSTERS_0_KSQLDBSERVERSSL_KEYSTOREPASSWORD` |Password of the JKS keystore for KSQL DB -|`KAFKA_CLUSTERS_0_KSQLDBSERVERSSL_TRUSTSTORELOCATION` |Path to the JKS truststore to communicate to KSQL DB -|`KAFKA_CLUSTERS_0_KSQLDBSERVERSSL_TRUSTSTOREPASSWORD` |Password of the JKS truststore for KSQL DB |`KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL` |Security protocol to connect to the brokers. For SSL connection use "SSL", for plaintext connection don't set this environment variable |`KAFKA_CLUSTERS_0_SCHEMAREGISTRY` |SchemaRegistry's address |`KAFKA_CLUSTERS_0_SCHEMAREGISTRYAUTH_USERNAME` |SchemaRegistry's basic authentication username |`KAFKA_CLUSTERS_0_SCHEMAREGISTRYAUTH_PASSWORD` |SchemaRegistry's basic authentication password |`KAFKA_CLUSTERS_0_SCHEMAREGISTRYSSL_KEYSTORELOCATION` |Path to the JKS keystore to communicate to SchemaRegistry |`KAFKA_CLUSTERS_0_SCHEMAREGISTRYSSL_KEYSTOREPASSWORD` |Password of the JKS keystore for SchemaRegistry -|`KAFKA_CLUSTERS_0_SCHEMAREGISTRYSSL_TRUSTSTORELOCATION` |Path to the JKS truststore to communicate to SchemaRegistry -|`KAFKA_CLUSTERS_0_SCHEMAREGISTRYSSL_TRUSTSTOREPASSWORD` |Password of the JKS truststore for SchemaRegistry +|`KAFKA_CLUSTERS_0_METRICS_SSL` |Enable SSL for Metrics (for PROMETHEUS metrics type). Default: false. +|`KAFKA_CLUSTERS_0_METRICS_USERNAME` |Username for Metrics authentication +|`KAFKA_CLUSTERS_0_METRICS_PASSWORD` |Password for Metrics authentication +|`KAFKA_CLUSTERS_0_METRICS_KEYSTORELOCATION` |Path to the JKS keystore to communicate to metrics source (JMX/PROMETHEUS). For advanced setup, see `kafka-ui-jmx-secured.yml` +|`KAFKA_CLUSTERS_0_METRICS_KEYSTOREPASSWORD` |Password of the JKS metrics keystore |`KAFKA_CLUSTERS_0_SCHEMANAMETEMPLATE` |How keys are saved to schemaRegistry |`KAFKA_CLUSTERS_0_METRICS_PORT` |Open metrics port of a broker |`KAFKA_CLUSTERS_0_METRICS_TYPE` |Type of metrics retriever to use. Valid values are JMX (default) or PROMETHEUS. If Prometheus, then metrics are read from prometheus-jmx-exporter instead of jmx |`KAFKA_CLUSTERS_0_READONLY` |Enable read-only mode. Default: false -|`KAFKA_CLUSTERS_0_DISABLELOGDIRSCOLLECTION` |Disable collecting segments information. It should be true for confluent cloud. Default: false |`KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME` |Given name for the Kafka Connect cluster |`KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS` |Address of the Kafka Connect service endpoint |`KAFKA_CLUSTERS_0_KAFKACONNECT_0_USERNAME`| Kafka Connect cluster's basic authentication username |`KAFKA_CLUSTERS_0_KAFKACONNECT_0_PASSWORD`| Kafka Connect cluster's basic authentication password |`KAFKA_CLUSTERS_0_KAFKACONNECT_0_KEYSTORELOCATION`| Path to the JKS keystore to communicate to Kafka Connect |`KAFKA_CLUSTERS_0_KAFKACONNECT_0_KEYSTOREPASSWORD`| Password of the JKS keystore for Kafka Connect -|`KAFKA_CLUSTERS_0_KAFKACONNECT_0_TRUSTSTORELOCATION`| Path to the JKS truststore to communicate to Kafka Connect -|`KAFKA_CLUSTERS_0_KAFKACONNECT_0_TRUSTSTOREPASSWORD`| Password of the JKS truststore for Kafka Connect -|`KAFKA_CLUSTERS_0_METRICS_SSL` |Enable SSL for Metrics? `true` or `false`. For advanced setup, see `kafka-ui-jmx-secured.yml` -|`KAFKA_CLUSTERS_0_METRICS_USERNAME` |Username for Metrics authentication -|`KAFKA_CLUSTERS_0_METRICS_PASSWORD` |Password for Metrics authentication |`KAFKA_CLUSTERS_0_POLLING_THROTTLE_RATE` |Max traffic rate (bytes/sec) that kafka-ui allowed to reach when polling messages from the cluster. Default: 0 (not limited) +|`KAFKA_CLUSTERS_0_SSL_TRUSTSTORELOCATION`| Path to the JKS truststore to communicate to Kafka Connect, SchemaRegistry, KSQL, Metrics +|`KAFKA_CLUSTERS_0_SSL_TRUSTSTOREPASSWORD`| Password of the JKS truststore for Kafka Connect, SchemaRegistry, KSQL, Metrics |`TOPIC_RECREATE_DELAY_SECONDS` |Time delay between topic deletion and topic creation attempts for topic recreate functionality. Default: 1 |`TOPIC_RECREATE_MAXRETRIES` |Number of attempts of topic creation after topic deletion for topic recreate functionality. Default: 15 +|`DYNAMIC_CONFIG_ENABLED`|Allow to change application config in runtime. Default: false. diff --git a/charts/kafka-ui/Chart.yaml b/charts/kafka-ui/Chart.yaml index 28e1a892aa..553d193b79 100644 --- a/charts/kafka-ui/Chart.yaml +++ b/charts/kafka-ui/Chart.yaml @@ -2,6 +2,6 @@ apiVersion: v2 name: kafka-ui description: A Helm chart for kafka-UI type: application -version: 0.5.1 -appVersion: v0.5.0 +version: 0.6.0 +appVersion: v0.6.0 icon: https://github.com/provectus/kafka-ui/raw/master/documentation/images/kafka-ui-logo.png diff --git a/charts/kafka-ui/templates/_helpers.tpl b/charts/kafka-ui/templates/_helpers.tpl index 510452d4cf..7155681a44 100644 --- a/charts/kafka-ui/templates/_helpers.tpl +++ b/charts/kafka-ui/templates/_helpers.tpl @@ -68,6 +68,11 @@ This allows us to check if the registry of the image is specified or not. */}} {{- define "kafka-ui.imageName" -}} {{- $registryName := .Values.image.registry -}} +{{- if .Values.global }} + {{- if .Values.global.imageRegistry }} + {{- $registryName = .Values.global.imageRegistry -}} + {{- end -}} +{{- end -}} {{- $repository := .Values.image.repository -}} {{- $tag := .Values.image.tag | default .Chart.AppVersion -}} {{- if $registryName }} diff --git a/charts/kafka-ui/templates/ingress.yaml b/charts/kafka-ui/templates/ingress.yaml index e4b33439c4..13e746d8d1 100644 --- a/charts/kafka-ui/templates/ingress.yaml +++ b/charts/kafka-ui/templates/ingress.yaml @@ -35,7 +35,7 @@ spec: {{- if and ($.Capabilities.APIVersions.Has "networking.k8s.io/v1") $isHigher1p19 -}} {{- range .Values.ingress.precedingPaths }} - path: {{ .path }} - pathType: Prefix + pathType: {{ .Values.ingress.pathType }} backend: service: name: {{ .serviceName }} @@ -47,13 +47,13 @@ spec: name: {{ $fullName }} port: number: {{ $svcPort }} - pathType: Prefix + pathType: {{ .Values.ingress.pathType }} {{- if .Values.ingress.path }} path: {{ .Values.ingress.path }} {{- end }} {{- range .Values.ingress.succeedingPaths }} - path: {{ .path }} - pathType: Prefix + pathType: {{ .Values.ingress.pathType }} backend: service: name: {{ .serviceName }} diff --git a/charts/kafka-ui/templates/secret.yaml b/charts/kafka-ui/templates/secret.yaml index a2d1f25fa2..1f974503dc 100644 --- a/charts/kafka-ui/templates/secret.yaml +++ b/charts/kafka-ui/templates/secret.yaml @@ -1,3 +1,4 @@ +{{- if .Values.envs.secret -}} apiVersion: v1 kind: Secret metadata: @@ -9,3 +10,4 @@ data: {{- range $key, $val := .Values.envs.secret }} {{ $key }}: {{ $val | b64enc | quote }} {{- end -}} +{{- end}} \ No newline at end of file diff --git a/charts/kafka-ui/values.yaml b/charts/kafka-ui/values.yaml index dce32059e1..3c30b40813 100644 --- a/charts/kafka-ui/values.yaml +++ b/charts/kafka-ui/values.yaml @@ -111,6 +111,9 @@ ingress: # The path for the Ingress path: "/" + # The path type for the Ingress + pathType: "Prefix" + # The hostname for the Ingress host: "" diff --git a/documentation/compose/jaas/client.properties b/documentation/compose/jaas/client.properties old mode 100644 new mode 100755 diff --git a/documentation/compose/jaas/kafka_connect.jaas b/documentation/compose/jaas/kafka_connect.jaas old mode 100644 new mode 100755 diff --git a/documentation/compose/jaas/kafka_connect.password b/documentation/compose/jaas/kafka_connect.password old mode 100644 new mode 100755 diff --git a/documentation/compose/jaas/kafka_server.conf b/documentation/compose/jaas/kafka_server.conf index ef41c992e2..25388be5aa 100644 --- a/documentation/compose/jaas/kafka_server.conf +++ b/documentation/compose/jaas/kafka_server.conf @@ -11,4 +11,4 @@ KafkaClient { user_admin="admin-secret"; }; -Client {}; \ No newline at end of file +Client {}; diff --git a/documentation/compose/jaas/schema_registry.jaas b/documentation/compose/jaas/schema_registry.jaas old mode 100644 new mode 100755 diff --git a/documentation/compose/jaas/schema_registry.password b/documentation/compose/jaas/schema_registry.password old mode 100644 new mode 100755 diff --git a/documentation/compose/kafka-ssl-components.yaml b/documentation/compose/kafka-ssl-components.yaml index 0c1287b647..407ce5b97a 100644 --- a/documentation/compose/kafka-ssl-components.yaml +++ b/documentation/compose/kafka-ssl-components.yaml @@ -15,27 +15,25 @@ services: KAFKA_CLUSTERS_0_NAME: local KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL: SSL KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 # SSL LISTENER! - KAFKA_CLUSTERS_0_PROPERTIES_SSL_TRUSTSTORE_LOCATION: /kafka.truststore.jks - KAFKA_CLUSTERS_0_PROPERTIES_SSL_TRUSTSTORE_PASSWORD: secret - KAFKA_CLUSTERS_0_PROPERTIES_SSL_KEYSTORE_LOCATION: /kafka.keystore.jks - KAFKA_CLUSTERS_0_PROPERTIES_SSL_KEYSTORE_PASSWORD: secret KAFKA_CLUSTERS_0_PROPERTIES_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: '' # DISABLE COMMON NAME VERIFICATION + KAFKA_CLUSTERS_0_SCHEMAREGISTRY: https://schemaregistry0:8085 KAFKA_CLUSTERS_0_SCHEMAREGISTRYSSL_KEYSTORELOCATION: /kafka.keystore.jks KAFKA_CLUSTERS_0_SCHEMAREGISTRYSSL_KEYSTOREPASSWORD: "secret" - KAFKA_CLUSTERS_0_SCHEMAREGISTRYSSL_TRUSTSTORELOCATION: /kafka.truststore.jks - KAFKA_CLUSTERS_0_SCHEMAREGISTRYSSL_TRUSTSTOREPASSWORD: "secret" + KAFKA_CLUSTERS_0_KSQLDBSERVER: https://ksqldb0:8088 KAFKA_CLUSTERS_0_KSQLDBSERVERSSL_KEYSTORELOCATION: /kafka.keystore.jks KAFKA_CLUSTERS_0_KSQLDBSERVERSSL_KEYSTOREPASSWORD: "secret" - KAFKA_CLUSTERS_0_KSQLDBSERVERSSL_TRUSTSTORELOCATION: /kafka.truststore.jks - KAFKA_CLUSTERS_0_KSQLDBSERVERSSL_TRUSTSTOREPASSWORD: "secret" + KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: local KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: https://kafka-connect0:8083 KAFKA_CLUSTERS_0_KAFKACONNECT_0_KEYSTORELOCATION: /kafka.keystore.jks KAFKA_CLUSTERS_0_KAFKACONNECT_0_KEYSTOREPASSWORD: "secret" - KAFKA_CLUSTERS_0_KAFKACONNECT_0_TRUSTSTORELOCATION: /kafka.truststore.jks - KAFKA_CLUSTERS_0_KAFKACONNECT_0_TRUSTSTOREPASSWORD: "secret" + + KAFKA_CLUSTERS_0_SSL_TRUSTSTORELOCATION: /kafka.truststore.jks + KAFKA_CLUSTERS_0_SSL_TRUSTSTOREPASSWORD: "secret" + DYNAMIC_CONFIG_ENABLED: 'true' # not necessary for ssl, added for tests + volumes: - ./ssl/kafka.truststore.jks:/kafka.truststore.jks - ./ssl/kafka.keystore.jks:/kafka.keystore.jks diff --git a/documentation/compose/kafka-ssl.yml b/documentation/compose/kafka-ssl.yml index 4fc7daebff..08ff9dc4af 100644 --- a/documentation/compose/kafka-ssl.yml +++ b/documentation/compose/kafka-ssl.yml @@ -11,11 +11,11 @@ services: environment: KAFKA_CLUSTERS_0_NAME: local KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL: SSL - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 # SSL LISTENER! - KAFKA_CLUSTERS_0_PROPERTIES_SSL_TRUSTSTORE_LOCATION: /kafka.truststore.jks - KAFKA_CLUSTERS_0_PROPERTIES_SSL_TRUSTSTORE_PASSWORD: secret KAFKA_CLUSTERS_0_PROPERTIES_SSL_KEYSTORE_LOCATION: /kafka.keystore.jks - KAFKA_CLUSTERS_0_PROPERTIES_SSL_KEYSTORE_PASSWORD: secret + KAFKA_CLUSTERS_0_PROPERTIES_SSL_KEYSTORE_PASSWORD: "secret" + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 # SSL LISTENER! + KAFKA_CLUSTERS_0_SSL_TRUSTSTORELOCATION: /kafka.truststore.jks + KAFKA_CLUSTERS_0_SSL_TRUSTSTOREPASSWORD: "secret" KAFKA_CLUSTERS_0_PROPERTIES_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: '' # DISABLE COMMON NAME VERIFICATION volumes: - ./ssl/kafka.truststore.jks:/kafka.truststore.jks @@ -60,4 +60,4 @@ services: - ./ssl/creds:/etc/kafka/secrets/creds - ./ssl/kafka.truststore.jks:/etc/kafka/secrets/kafka.truststore.jks - ./ssl/kafka.keystore.jks:/etc/kafka/secrets/kafka.keystore.jks - command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" \ No newline at end of file + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" diff --git a/documentation/compose/kafka-ui-arm64.yaml b/documentation/compose/kafka-ui-arm64.yaml index bbcefecbf4..87a892cc70 100644 --- a/documentation/compose/kafka-ui-arm64.yaml +++ b/documentation/compose/kafka-ui-arm64.yaml @@ -19,6 +19,7 @@ services: KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schema-registry0:8085 KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083 + DYNAMIC_CONFIG_ENABLED: 'true' # not necessary, added for tests kafka0: image: confluentinc/cp-kafka:7.2.1.arm64 diff --git a/documentation/compose/kafka-ui-jmx-secured.yml b/documentation/compose/kafka-ui-jmx-secured.yml index de56a7e2c6..408f388ba5 100644 --- a/documentation/compose/kafka-ui-jmx-secured.yml +++ b/documentation/compose/kafka-ui-jmx-secured.yml @@ -7,11 +7,8 @@ services: image: provectuslabs/kafka-ui:latest ports: - 8080:8080 - - 5005:5005 depends_on: - kafka0 - - schemaregistry0 - - kafka-connect0 environment: KAFKA_CLUSTERS_0_NAME: local KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 @@ -19,15 +16,12 @@ services: KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083 KAFKA_CLUSTERS_0_METRICS_PORT: 9997 - KAFKA_CLUSTERS_0_METRICS_SSL: 'true' KAFKA_CLUSTERS_0_METRICS_USERNAME: root KAFKA_CLUSTERS_0_METRICS_PASSWORD: password - JAVA_OPTS: >- - -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 - -Djavax.net.ssl.trustStore=/jmx/clienttruststore - -Djavax.net.ssl.trustStorePassword=12345678 - -Djavax.net.ssl.keyStore=/jmx/clientkeystore - -Djavax.net.ssl.keyStorePassword=12345678 + KAFKA_CLUSTERS_0_METRICS_KEYSTORE_LOCATION: /jmx/clientkeystore + KAFKA_CLUSTERS_0_METRICS_KEYSTORE_PASSWORD: '12345678' + KAFKA_CLUSTERS_0_SSL_TRUSTSTORE_LOCATION: /jmx/clienttruststore + KAFKA_CLUSTERS_0_SSL_TRUSTSTORE_PASSWORD: '12345678' volumes: - ./jmx/clienttruststore:/jmx/clienttruststore - ./jmx/clientkeystore:/jmx/clientkeystore @@ -70,8 +64,6 @@ services: -Dcom.sun.management.jmxremote.access.file=/jmx/jmxremote.access -Dcom.sun.management.jmxremote.rmi.port=9997 -Djava.rmi.server.hostname=kafka0 - -Djava.rmi.server.logCalls=true -# -Djavax.net.debug=ssl:handshake volumes: - ./jmx/serverkeystore:/jmx/serverkeystore - ./jmx/servertruststore:/jmx/servertruststore @@ -79,56 +71,3 @@ services: - ./jmx/jmxremote.access:/jmx/jmxremote.access - ./scripts/update_run.sh:/tmp/update_run.sh command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" - - schemaregistry0: - image: confluentinc/cp-schema-registry:7.2.1 - ports: - - 8085:8085 - depends_on: - - kafka0 - environment: - SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092 - SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT - SCHEMA_REGISTRY_HOST_NAME: schemaregistry0 - SCHEMA_REGISTRY_LISTENERS: http://schemaregistry0:8085 - - SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http" - SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO - SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas - - kafka-connect0: - image: confluentinc/cp-kafka-connect:7.2.1 - ports: - - 8083:8083 - depends_on: - - kafka0 - - schemaregistry0 - environment: - CONNECT_BOOTSTRAP_SERVERS: kafka0:29092 - CONNECT_GROUP_ID: compose-connect-group - CONNECT_CONFIG_STORAGE_TOPIC: _connect_configs - CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1 - CONNECT_OFFSET_STORAGE_TOPIC: _connect_offset - CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1 - CONNECT_STATUS_STORAGE_TOPIC: _connect_status - CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1 - CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter - CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085 - CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.storage.StringConverter - CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085 - CONNECT_INTERNAL_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter - CONNECT_INTERNAL_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter - CONNECT_REST_ADVERTISED_HOST_NAME: kafka-connect0 - CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components" - - kafka-init-topics: - image: confluentinc/cp-kafka:7.2.1 - volumes: - - ./message.json:/data/message.json - depends_on: - - kafka0 - command: "bash -c 'echo Waiting for Kafka to be ready... && \ - cub kafka-ready -b kafka0:29092 1 30 && \ - kafka-topics --create --topic second.users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \ - kafka-topics --create --topic first.messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \ - kafka-console-producer --bootstrap-server kafka0:29092 --topic second.users < /data/message.json'" \ No newline at end of file diff --git a/documentation/compose/kafka-ui-sasl.yaml b/documentation/compose/kafka-ui-sasl.yaml index 6dfe4f7532..e4a2b3cc4a 100644 --- a/documentation/compose/kafka-ui-sasl.yaml +++ b/documentation/compose/kafka-ui-sasl.yaml @@ -8,43 +8,45 @@ services: ports: - 8080:8080 depends_on: - - zookeeper - kafka environment: KAFKA_CLUSTERS_0_NAME: local - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9093 - KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181 + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL: SASL_PLAINTEXT KAFKA_CLUSTERS_0_PROPERTIES_SASL_MECHANISM: PLAIN KAFKA_CLUSTERS_0_PROPERTIES_SASL_JAAS_CONFIG: 'org.apache.kafka.common.security.plain.PlainLoginModule required username="admin" password="admin-secret";' - - zookeeper: - image: wurstmeister/zookeeper:3.4.6 - environment: - JVMFLAGS: "-Djava.security.auth.login.config=/etc/zookeeper/zookeeper_jaas.conf" - volumes: - - ./jaas/zookeeper_jaas.conf:/etc/zookeeper/zookeeper_jaas.conf - ports: - - 2181:2181 + DYNAMIC_CONFIG_ENABLED: true # not necessary for sasl auth, added for tests kafka: - image: wurstmeister/kafka:2.13-2.8.1 - depends_on: - - zookeeper + image: confluentinc/cp-kafka:7.2.1 + hostname: kafka + container_name: kafka ports: - - 9092:9092 + - "9092:9092" + - "9997:9997" environment: - KAFKA_AUTHORIZER_CLASS_NAME: kafka.security.authorizer.AclAuthorizer - KAFKA_SUPER_USERS: "User:admin" - KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_LISTENERS: INTERNAL://:9093,EXTERNAL://:9092 - KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:9093,EXTERNAL://localhost:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:SASL_PLAINTEXT,EXTERNAL:SASL_PLAINTEXT - ALLOW_PLAINTEXT_LISTENER: 'yes' - KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' - KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL - KAFKA_SASL_ENABLED_MECHANISMS: PLAIN - KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL: PLAIN - KAFKA_OPTS: "-Djava.security.auth.login.config=/etc/kafka/kafka_jaas.conf" + KAFKA_BROKER_ID: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' + KAFKA_ADVERTISED_LISTENERS: 'SASL_PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092' + KAFKA_OPTS: "-Djava.security.auth.login.config=/etc/kafka/jaas/kafka_server.conf" + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9997 + KAFKA_JMX_HOSTNAME: localhost + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093' + KAFKA_LISTENERS: 'SASL_PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_INTER_BROKER_LISTENER_NAME: 'SASL_PLAINTEXT' + KAFKA_SASL_ENABLED_MECHANISMS: 'PLAIN' + KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL: 'PLAIN' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + KAFKA_SECURITY_PROTOCOL: 'SASL_PLAINTEXT' + KAFKA_SUPER_USERS: 'User:admin,User:enzo' volumes: - - ./jaas/kafka_server.conf:/etc/kafka/kafka_jaas.conf \ No newline at end of file + - ./scripts/update_run.sh:/tmp/update_run.sh + - ./jaas:/etc/kafka/jaas + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" diff --git a/documentation/compose/kafka-ui-serdes.yaml b/documentation/compose/kafka-ui-serdes.yaml index 143d454bb5..eee510a13d 100644 --- a/documentation/compose/kafka-ui-serdes.yaml +++ b/documentation/compose/kafka-ui-serdes.yaml @@ -14,13 +14,16 @@ services: kafka.clusters.0.name: SerdeExampleCluster kafka.clusters.0.bootstrapServers: kafka0:29092 kafka.clusters.0.schemaRegistry: http://schemaregistry0:8085 - # optional auth and ssl properties for SR + + # optional SSL settings for cluster (will be used by SchemaRegistry serde, if set) + #kafka.clusters.0.ssl.keystoreLocation: /kafka.keystore.jks + #kafka.clusters.0.ssl.keystorePassword: "secret" + #kafka.clusters.0.ssl.truststoreLocation: /kafka.truststore.jks + #kafka.clusters.0.ssl.truststorePassword: "secret" + + # optional auth properties for SR #kafka.clusters.0.schemaRegistryAuth.username: "use" #kafka.clusters.0.schemaRegistryAuth.password: "pswrd" - #kafka.clusters.0.schemaRegistrySSL.keystoreLocation: /kafka.keystore.jks - #kafka.clusters.0.schemaRegistrySSL.keystorePassword: "secret" - #kafka.clusters.0.schemaRegistrySSL.truststoreLocation: /kafka.truststore.jks - #kafka.clusters.0.schemaRegistrySSL.truststorePassword: "secret" kafka.clusters.0.defaultKeySerde: Int32 #optional kafka.clusters.0.defaultValueSerde: String #optional @@ -28,8 +31,7 @@ services: kafka.clusters.0.serde.0.name: ProtobufFile kafka.clusters.0.serde.0.topicKeysPattern: "topic1" kafka.clusters.0.serde.0.topicValuesPattern: "topic1" - kafka.clusters.0.serde.0.properties.protobufFiles.0: /protofiles/key-types.proto - kafka.clusters.0.serde.0.properties.protobufFiles.1: /protofiles/values.proto + kafka.clusters.0.serde.0.properties.protobufFilesDir: /protofiles/ kafka.clusters.0.serde.0.properties.protobufMessageNameForKey: test.MyKey # default type for keys kafka.clusters.0.serde.0.properties.protobufMessageName: test.MyValue # default type for values kafka.clusters.0.serde.0.properties.protobufMessageNameForKeyByTopic.topic1: test.MySpecificTopicKey # keys type for topic "topic1" @@ -52,7 +54,7 @@ services: kafka.clusters.0.serde.4.properties.keySchemaNameTemplate: "%s-key" kafka.clusters.0.serde.4.properties.schemaNameTemplate: "%s-value" #kafka.clusters.0.serde.4.topicValuesPattern: "sr2-topic.*" - # optional auth and ssl properties for SR: + # optional auth and ssl properties for SR (overrides cluster-level): #kafka.clusters.0.serde.4.properties.username: "user" #kafka.clusters.0.serde.4.properties.password: "passw" #kafka.clusters.0.serde.4.properties.keystoreLocation: /kafka.keystore.jks diff --git a/documentation/compose/kafka-ui.yaml b/documentation/compose/kafka-ui.yaml index 32c874b676..8524f6fa2b 100644 --- a/documentation/compose/kafka-ui.yaml +++ b/documentation/compose/kafka-ui.yaml @@ -24,6 +24,7 @@ services: KAFKA_CLUSTERS_1_BOOTSTRAPSERVERS: kafka1:29092 KAFKA_CLUSTERS_1_METRICS_PORT: 9998 KAFKA_CLUSTERS_1_SCHEMAREGISTRY: http://schemaregistry1:8085 + DYNAMIC_CONFIG_ENABLED: 'true' kafka0: image: confluentinc/cp-kafka:7.2.1 diff --git a/documentation/compose/proto/key-types.proto b/documentation/compose/proto/key-types.proto index 908aed5689..1f5e22a427 100644 --- a/documentation/compose/proto/key-types.proto +++ b/documentation/compose/proto/key-types.proto @@ -1,11 +1,15 @@ syntax = "proto3"; package test; +import "google/protobuf/wrappers.proto"; + message MyKey { string myKeyF1 = 1; + google.protobuf.UInt64Value uint_64_wrapper = 2; } message MySpecificTopicKey { string special_field1 = 1; string special_field2 = 2; + google.protobuf.FloatValue float_wrapper = 3; } diff --git a/documentation/guides/Protobuf.md b/documentation/guides/Protobuf.md index 533a6a8f48..12f92448c8 100644 --- a/documentation/guides/Protobuf.md +++ b/documentation/guides/Protobuf.md @@ -12,22 +12,26 @@ To configure Kafkaui to deserialize protobuf messages using a supplied protobuf ```yaml kafka: clusters: - - # Cluster configuration omitted. - # protobufFile is the path to the protobuf schema. (deprecated: please use "protobufFiles") + - # Cluster configuration omitted... + # protobufFilesDir specifies root location for proto files (will be scanned recursively) + # NOTE: if 'protobufFilesDir' specified, then 'protobufFile' and 'protobufFiles' settings will be ignored + protobufFilesDir: "/path/to/my-protobufs" + # (DEPRECATED) protobufFile is the path to the protobuf schema. (deprecated: please use "protobufFiles") protobufFile: path/to/my.proto - # protobufFiles is the path to one or more protobuf schemas. - protobufFiles: - - /path/to/my.proto - - /path/to/another.proto - # protobufMessageName is the default protobuf type that is used to deserilize - # the message's value if the topic is not found in protobufMessageNameByTopic. + # (DEPRECATED) protobufFiles is the location of one or more protobuf schemas + protobufFiles: + - /path/to/my-protobufs/my.proto + - /path/to/my-protobufs/another.proto + - /path/to/my-protobufs:test/test.proto + # protobufMessageName is the default protobuf type that is used to deserialize + # the message's value if the topic is not found in protobufMessageNameByTopic. protobufMessageName: my.DefaultValType # protobufMessageNameByTopic is a mapping of topic names to protobuf types. # This mapping is required and is used to deserialize the Kafka message's value. protobufMessageNameByTopic: topic1: my.Type1 topic2: my.Type2 - # protobufMessageNameForKey is the default protobuf type that is used to deserilize + # protobufMessageNameForKey is the default protobuf type that is used to deserialize # the message's key if the topic is not found in protobufMessageNameForKeyByTopic. protobufMessageNameForKey: my.DefaultKeyType # protobufMessageNameForKeyByTopic is a mapping of topic names to protobuf types. diff --git a/documentation/guides/Serialization.md b/documentation/guides/Serialization.md index 19869a8bc1..b9690f2cba 100644 --- a/documentation/guides/Serialization.md +++ b/documentation/guides/Serialization.md @@ -20,7 +20,7 @@ kafka: clusters: - name: Cluster1 # Other Cluster configuration omitted ... - serdes: + serde: # registering String serde with custom config - name: AsciiString className: com.provectus.kafka.ui.serdes.builtin.StringSerde @@ -43,13 +43,11 @@ kafka: clusters: - name: Cluster1 # Other Cluster configuration omitted ... - serdes: + serde: - name: ProtobufFile properties: - # path to the protobuf schema files - protobufFiles: - - path/to/my.proto - - path/to/another.proto + # path to the protobuf schema files directory + protobufFilesDir: "path/to/protofiles" # default protobuf type that is used for KEY serialization/deserialization # optional protobufMessageNameForKey: my.Type1 @@ -84,7 +82,7 @@ kafka: - name: Cluster1 # this url will be used by "SchemaRegistry" by default schemaRegistry: http://main-schema-registry:8081 - serdes: + serde: - name: AnotherSchemaRegistry className: com.provectus.kafka.ui.serdes.builtin.sr.SchemaRegistrySerde properties: @@ -109,7 +107,7 @@ Sample configuration: kafka: clusters: - name: Cluster1 - serdes: + serde: - name: String topicKeysPattern: click-events|imp-events @@ -131,7 +129,7 @@ kafka: - name: Cluster1 defaultKeySerde: Int32 defaultValueSerde: String - serdes: + serde: - name: Int32 topicKeysPattern: click-events|imp-events ``` @@ -156,7 +154,7 @@ Sample configuration: kafka: clusters: - name: Cluster1 - serdes: + serde: - name: MyCustomSerde className: my.lovely.org.KafkaUiSerde filePath: /var/lib/kui-serde/my-kui-serde.jar diff --git a/kafka-ui-api/Dockerfile b/kafka-ui-api/Dockerfile index 96e0eb414b..fcd29c0f06 100644 --- a/kafka-ui-api/Dockerfile +++ b/kafka-ui-api/Dockerfile @@ -1,8 +1,12 @@ -FROM azul/zulu-openjdk-alpine:17 +FROM azul/zulu-openjdk-alpine:17-jre RUN apk add --no-cache gcompat # need to make snappy codec work RUN addgroup -S kafkaui && adduser -S kafkaui -G kafkaui +# creating folder for dynamic config usage (certificates uploads, etc) +RUN mkdir /etc/kafkaui/ +RUN chown kafkaui /etc/kafkaui + USER kafkaui ARG JAR_FILE @@ -12,4 +16,5 @@ ENV JAVA_OPTS= EXPOSE 8080 -CMD java $JAVA_OPTS -jar kafka-ui-api.jar +# see JmxSslSocketFactory docs to understand why add-opens is needed +CMD java --add-opens java.rmi/javax.rmi.ssl=ALL-UNNAMED $JAVA_OPTS -jar kafka-ui-api.jar diff --git a/kafka-ui-api/pom.xml b/kafka-ui-api/pom.xml index 8764020693..e61827fd73 100644 --- a/kafka-ui-api/pom.xml +++ b/kafka-ui-api/pom.xml @@ -199,6 +199,31 @@ ${antlr4-maven-plugin.version} + + org.opendatadiscovery + oddrn-generator-java + ${odd-oddrn-generator.version} + + + org.opendatadiscovery + ingestion-contract-client + + + org.springframework.boot + spring-boot-starter-webflux + + + io.projectreactor + reactor-core + + + io.projectreactor.ipc + reactor-netty + + + ${odd-oddrn-client.version} + + org.springframework.security spring-security-ldap diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/KafkaUiApplication.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/KafkaUiApplication.java index a9a523eb85..8d0eafeff3 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/KafkaUiApplication.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/KafkaUiApplication.java @@ -1,8 +1,10 @@ package com.provectus.kafka.ui; -import org.springframework.boot.SpringApplication; +import com.provectus.kafka.ui.util.DynamicConfigOperations; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @@ -12,6 +14,13 @@ import org.springframework.scheduling.annotation.EnableScheduling; public class KafkaUiApplication { public static void main(String[] args) { - SpringApplication.run(KafkaUiApplication.class, args); + startApplication(args); + } + + public static ConfigurableApplicationContext startApplication(String[] args) { + return new SpringApplicationBuilder(KafkaUiApplication.class) + .initializers(DynamicConfigOperations.dynamicConfigPropertiesInitializer()) + .build() + .run(args); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/RetryingKafkaConnectClient.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/RetryingKafkaConnectClient.java index be5686e2f9..5ec5a779d3 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/RetryingKafkaConnectClient.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/RetryingKafkaConnectClient.java @@ -2,6 +2,7 @@ package com.provectus.kafka.ui.client; import static com.provectus.kafka.ui.config.ClustersProperties.ConnectCluster; +import com.provectus.kafka.ui.config.ClustersProperties; import com.provectus.kafka.ui.connect.ApiClient; import com.provectus.kafka.ui.connect.api.KafkaConnectClientApi; import com.provectus.kafka.ui.connect.model.Connector; @@ -12,6 +13,7 @@ import com.provectus.kafka.ui.util.WebClientConfigurator; import java.time.Duration; import java.util.List; import java.util.Map; +import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; @@ -31,8 +33,10 @@ public class RetryingKafkaConnectClient extends KafkaConnectClientApi { private static final int MAX_RETRIES = 5; private static final Duration RETRIES_DELAY = Duration.ofMillis(200); - public RetryingKafkaConnectClient(ConnectCluster config, DataSize maxBuffSize) { - super(new RetryingApiClient(config, maxBuffSize)); + public RetryingKafkaConnectClient(ConnectCluster config, + @Nullable ClustersProperties.TruststoreConfig truststoreConfig, + DataSize maxBuffSize) { + super(new RetryingApiClient(config, truststoreConfig, maxBuffSize)); } private static Retry conflictCodeRetry() { @@ -77,23 +81,28 @@ public class RetryingKafkaConnectClient extends KafkaConnectClientApi { private static class RetryingApiClient extends ApiClient { - public RetryingApiClient(ConnectCluster config, DataSize maxBuffSize) { - super(buildWebClient(maxBuffSize, config), null, null); + public RetryingApiClient(ConnectCluster config, + ClustersProperties.TruststoreConfig truststoreConfig, + DataSize maxBuffSize) { + super(buildWebClient(maxBuffSize, config, truststoreConfig), null, null); setBasePath(config.getAddress()); - setUsername(config.getUserName()); + setUsername(config.getUsername()); setPassword(config.getPassword()); } - public static WebClient buildWebClient(DataSize maxBuffSize, ConnectCluster config) { + public static WebClient buildWebClient(DataSize maxBuffSize, + ConnectCluster config, + ClustersProperties.TruststoreConfig truststoreConfig) { return new WebClientConfigurator() .configureSsl( - config.getKeystoreLocation(), - config.getKeystorePassword(), - config.getTruststoreLocation(), - config.getTruststorePassword() + truststoreConfig, + new ClustersProperties.KeystoreConfig( + config.getKeystoreLocation(), + config.getKeystorePassword() + ) ) .configureBasicAuth( - config.getUserName(), + config.getUsername(), config.getPassword() ) .configureBufferSize(maxBuffSize) diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java index e709f33d4f..2cd5e0e69c 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java @@ -1,12 +1,13 @@ package com.provectus.kafka.ui.config; +import com.provectus.kafka.ui.model.MetricsConfig; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.Set; +import javax.annotation.Nullable; import javax.annotation.PostConstruct; import lombok.AllArgsConstructor; import lombok.Builder; @@ -30,56 +31,58 @@ public class ClustersProperties { String bootstrapServers; String schemaRegistry; SchemaRegistryAuth schemaRegistryAuth; - WebClientSsl schemaRegistrySsl; + KeystoreConfig schemaRegistrySsl; String ksqldbServer; KsqldbServerAuth ksqldbServerAuth; - WebClientSsl ksqldbServerSsl; + KeystoreConfig ksqldbServerSsl; List kafkaConnect; MetricsConfigData metrics; - Properties properties; + Map properties; boolean readOnly = false; - boolean disableLogDirsCollection = false; - List serde = new ArrayList<>(); + List serde; String defaultKeySerde; String defaultValueSerde; - List masking = new ArrayList<>(); - long pollingThrottleRate = 0; + List masking; + Long pollingThrottleRate; + TruststoreConfig ssl; } @Data + @ToString(exclude = "password") public static class MetricsConfigData { String type; Integer port; - boolean ssl; + Boolean ssl; String username; String password; + String keystoreLocation; + String keystorePassword; } @Data @NoArgsConstructor @AllArgsConstructor @Builder(toBuilder = true) + @ToString(exclude = {"password", "keystorePassword"}) public static class ConnectCluster { String name; String address; - String userName; + String username; String password; String keystoreLocation; String keystorePassword; - String truststoreLocation; - String truststorePassword; } @Data + @ToString(exclude = {"password"}) public static class SchemaRegistryAuth { String username; String password; } @Data - public static class WebClientSsl { - String keystoreLocation; - String keystorePassword; + @ToString(exclude = {"truststorePassword"}) + public static class TruststoreConfig { String truststoreLocation; String truststorePassword; } @@ -89,7 +92,7 @@ public class ClustersProperties { String name; String className; String filePath; - Map properties = new HashMap<>(); + Map properties; String topicKeysPattern; String topicValuesPattern; } @@ -101,12 +104,21 @@ public class ClustersProperties { String password; } + @Data + @NoArgsConstructor + @AllArgsConstructor + @ToString(exclude = {"keystorePassword"}) + public static class KeystoreConfig { + String keystoreLocation; + String keystorePassword; + } + @Data public static class Masking { Type type; - List fields = List.of(); //if empty - policy will be applied to all fields - List pattern = List.of("X", "x", "n", "-"); //used when type=MASK - String replacement = "***DATA_MASKED***"; //used when type=REPLACE + List fields; //if null or empty list - policy will be applied to all fields + List pattern; //used when type=MASK + String replacement; //used when type=REPLACE String topicKeysPattern; String topicValuesPattern; @@ -117,7 +129,41 @@ public class ClustersProperties { @PostConstruct public void validateAndSetDefaults() { - validateClusterNames(); + if (clusters != null) { + validateClusterNames(); + flattenClusterProperties(); + setMetricsDefaults(); + } + } + + private void setMetricsDefaults() { + for (Cluster cluster : clusters) { + if (cluster.getMetrics() != null && !StringUtils.hasText(cluster.getMetrics().getType())) { + cluster.getMetrics().setType(MetricsConfig.JMX_METRICS_TYPE); + } + } + } + + private void flattenClusterProperties() { + for (Cluster cluster : clusters) { + cluster.setProperties(flattenClusterProperties(null, cluster.getProperties())); + } + } + + private Map flattenClusterProperties(@Nullable String prefix, + @Nullable Map propertiesMap) { + Map flattened = new HashMap<>(); + if (propertiesMap != null) { + propertiesMap.forEach((k, v) -> { + String key = prefix == null ? k : prefix + "." + k; + if (v instanceof Map) { + flattened.putAll(flattenClusterProperties(key, (Map) v)); + } else { + flattened.put(key, v); + } + }); + } + return flattened; } private void validateClusterNames() { diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java index db192ae826..f79d217fa7 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java @@ -1,7 +1,6 @@ package com.provectus.kafka.ui.config.auth; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.annotation.PostConstruct; @@ -32,13 +31,13 @@ public class OAuthProperties { private String clientName; private String redirectUri; private String authorizationGrantType; - private Set scope = new HashSet<>(); + private Set scope; private String issuerUri; private String authorizationUri; private String tokenUri; private String userInfoUri; private String jwkSetUri; private String userNameAttribute; - private Map customParams = new HashMap<>(); + private Map customParams; } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthPropertiesConverter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthPropertiesConverter.java index 8e4a8575a8..90daa36273 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthPropertiesConverter.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthPropertiesConverter.java @@ -4,6 +4,8 @@ import static com.provectus.kafka.ui.config.auth.OAuthProperties.OAuth2Provider; import static org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Provider; import static org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Registration; +import java.util.Optional; +import java.util.Set; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.apache.commons.lang3.StringUtils; @@ -24,7 +26,7 @@ public final class OAuthPropertiesConverter { registration.setClientId(provider.getClientId()); registration.setClientSecret(provider.getClientSecret()); registration.setClientName(provider.getClientName()); - registration.setScope(provider.getScope()); + registration.setScope(Optional.ofNullable(provider.getScope()).orElse(Set.of())); registration.setRedirectUri(provider.getRedirectUri()); registration.setAuthorizationGrantType(provider.getAuthorizationGrantType()); @@ -71,7 +73,8 @@ public final class OAuthPropertiesConverter { } private static boolean isGoogle(OAuth2Provider provider) { - return GOOGLE.equalsIgnoreCase(provider.getCustomParams().get(TYPE)); + return provider.getCustomParams() != null + && GOOGLE.equalsIgnoreCase(provider.getCustomParams().get(TYPE)); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/CognitoLogoutSuccessHandler.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/CognitoLogoutSuccessHandler.java index 3d725c659d..e9e5159e1b 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/CognitoLogoutSuccessHandler.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/CognitoLogoutSuccessHandler.java @@ -12,6 +12,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.util.UrlUtils; import org.springframework.stereotype.Component; +import org.springframework.util.Assert; import org.springframework.web.server.WebSession; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -45,6 +46,10 @@ public class CognitoLogoutSuccessHandler implements LogoutSuccessHandler { .fragment(null) .build(); + Assert.isTrue( + provider.getCustomParams() != null && provider.getCustomParams().containsKey("logoutUrl"), + "Custom params should contain 'logoutUrl'" + ); final var uri = UriComponentsBuilder .fromUri(URI.create(provider.getCustomParams().get("logoutUrl"))) .queryParam("client_id", provider.getClientId()) diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AccessController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AccessController.java index 131a37982a..a86b6db5a0 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AccessController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AccessController.java @@ -66,7 +66,7 @@ public class AccessController implements AuthorizationApi { UserPermissionDTO dto = new UserPermissionDTO(); dto.setClusters(clusters); dto.setResource(ResourceTypeDTO.fromValue(permission.getResource().toString().toUpperCase())); - dto.setValue(permission.getValue() != null ? permission.getValue().toString() : null); + dto.setValue(permission.getValue()); dto.setActions(permission.getActions() .stream() .map(String::toUpperCase) diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ApplicationConfigController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ApplicationConfigController.java new file mode 100644 index 0000000000..b21ef10c61 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ApplicationConfigController.java @@ -0,0 +1,137 @@ +package com.provectus.kafka.ui.controller; + +import static com.provectus.kafka.ui.model.rbac.permission.ApplicationConfigAction.EDIT; +import static com.provectus.kafka.ui.model.rbac.permission.ApplicationConfigAction.VIEW; + +import com.provectus.kafka.ui.api.ApplicationConfigApi; +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.model.ApplicationConfigDTO; +import com.provectus.kafka.ui.model.ApplicationConfigPropertiesDTO; +import com.provectus.kafka.ui.model.ApplicationConfigValidationDTO; +import com.provectus.kafka.ui.model.ApplicationInfoDTO; +import com.provectus.kafka.ui.model.ClusterConfigValidationDTO; +import com.provectus.kafka.ui.model.RestartRequestDTO; +import com.provectus.kafka.ui.model.UploadedFileInfoDTO; +import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.service.KafkaClusterFactory; +import com.provectus.kafka.ui.service.rbac.AccessControlService; +import com.provectus.kafka.ui.util.ApplicationRestarter; +import com.provectus.kafka.ui.util.DynamicConfigOperations; +import com.provectus.kafka.ui.util.DynamicConfigOperations.PropertiesStructure; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import org.springframework.http.ResponseEntity; +import org.springframework.http.codec.multipart.FilePart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class ApplicationConfigController implements ApplicationConfigApi { + + private static final PropertiesMapper MAPPER = Mappers.getMapper(PropertiesMapper.class); + + @Mapper + interface PropertiesMapper { + + PropertiesStructure fromDto(ApplicationConfigPropertiesDTO dto); + + ApplicationConfigPropertiesDTO toDto(PropertiesStructure propertiesStructure); + } + + private final AccessControlService accessControlService; + private final DynamicConfigOperations dynamicConfigOperations; + private final ApplicationRestarter restarter; + private final KafkaClusterFactory kafkaClusterFactory; + + + @Override + public Mono> getApplicationInfo(ServerWebExchange exchange) { + return Mono.just( + new ApplicationInfoDTO() + .enabledFeatures( + dynamicConfigOperations.dynamicConfigEnabled() + ? List.of(ApplicationInfoDTO.EnabledFeaturesEnum.DYNAMIC_CONFIG) + : List.of() + ) + ).map(ResponseEntity::ok); + } + + @Override + public Mono> getCurrentConfig(ServerWebExchange exchange) { + return accessControlService + .validateAccess( + AccessContext.builder() + .applicationConfigActions(VIEW) + .build() + ) + .then(Mono.fromSupplier(() -> ResponseEntity.ok( + new ApplicationConfigDTO() + .properties(MAPPER.toDto(dynamicConfigOperations.getCurrentProperties())) + ))); + } + + @Override + public Mono> restartWithConfig(Mono restartRequestDto, + ServerWebExchange exchange) { + return accessControlService + .validateAccess( + AccessContext.builder() + .applicationConfigActions(EDIT) + .build() + ) + .then(restartRequestDto) + .map(dto -> { + dynamicConfigOperations.persist(MAPPER.fromDto(dto.getConfig().getProperties())); + restarter.requestRestart(); + return ResponseEntity.ok().build(); + }); + } + + @Override + public Mono> uploadConfigRelatedFile(FilePart file, ServerWebExchange exchange) { + return accessControlService + .validateAccess( + AccessContext.builder() + .applicationConfigActions(EDIT) + .build() + ) + .then(dynamicConfigOperations.uploadConfigRelatedFile(file)) + .map(path -> new UploadedFileInfoDTO().location(path.toString())) + .map(ResponseEntity::ok); + } + + @Override + public Mono> validateConfig(Mono configDto, + ServerWebExchange exchange) { + return configDto + .flatMap(config -> { + PropertiesStructure propertiesStructure = MAPPER.fromDto(config.getProperties()); + ClustersProperties clustersProperties = propertiesStructure.getKafka(); + return validateClustersConfig(clustersProperties) + .map(validations -> new ApplicationConfigValidationDTO().clusters(validations)); + }) + .map(ResponseEntity::ok); + } + + private Mono> validateClustersConfig( + @Nullable ClustersProperties properties) { + if (properties == null || properties.getClusters() == null) { + return Mono.just(Map.of()); + } + properties.validateAndSetDefaults(); + return Flux.fromIterable(properties.getClusters()) + .flatMap(c -> kafkaClusterFactory.validate(c).map(v -> Tuples.of(c.getName(), v))) + .collectMap(Tuple2::getT1, Tuple2::getT2); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KafkaConnectController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KafkaConnectController.java index 2f8bb2dbf4..9ffd901c07 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KafkaConnectController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KafkaConnectController.java @@ -37,10 +37,10 @@ public class KafkaConnectController extends AbstractController implements KafkaC public Mono>> getConnects(String clusterName, ServerWebExchange exchange) { - Flux flux = Flux.fromIterable(kafkaConnectService.getConnects(getCluster(clusterName))) + Flux availableConnects = kafkaConnectService.getConnects(getCluster(clusterName)) .filterWhen(dto -> accessControlService.isConnectAccessible(dto, clusterName)); - return Mono.just(ResponseEntity.ok(flux)); + return Mono.just(ResponseEntity.ok(availableConnects)); } @Override @@ -54,7 +54,7 @@ public class KafkaConnectController extends AbstractController implements KafkaC .build()); return validateAccess.thenReturn( - ResponseEntity.ok(kafkaConnectService.getConnectors(getCluster(clusterName), connectName)) + ResponseEntity.ok(kafkaConnectService.getConnectorNames(getCluster(clusterName), connectName)) ); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/TopicsController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/TopicsController.java index c9cfca18e8..a587d53770 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/TopicsController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/TopicsController.java @@ -175,7 +175,7 @@ public class TopicsController extends AbstractController implements TopicsApi { List filtered = existingTopics.stream() .filter(topic -> !topic.isInternal() || showInternal != null && showInternal) - .filter(topic -> search == null || StringUtils.contains(topic.getName(), search)) + .filter(topic -> search == null || StringUtils.containsIgnoreCase(topic.getName(), search)) .sorted(comparator) .toList(); var totalPages = (filtered.size() / pageSize) diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ErrorCode.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ErrorCode.java index 6c05eec206..61be8155e8 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ErrorCode.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ErrorCode.java @@ -29,7 +29,9 @@ public enum ErrorCode { RECREATE_TOPIC_TIMEOUT(4015, HttpStatus.REQUEST_TIMEOUT), INVALID_ENTITY_STATE(4016, HttpStatus.BAD_REQUEST), SCHEMA_NOT_DELETED(4017, HttpStatus.INTERNAL_SERVER_ERROR), - TOPIC_ANALYSIS_ERROR(4018, HttpStatus.BAD_REQUEST); + TOPIC_ANALYSIS_ERROR(4018, HttpStatus.BAD_REQUEST), + FILE_UPLOAD_EXCEPTION(4019, HttpStatus.INTERNAL_SERVER_ERROR), + ; static { // codes uniqueness check diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/FileUploadException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/FileUploadException.java new file mode 100644 index 0000000000..e5e410d64a --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/FileUploadException.java @@ -0,0 +1,19 @@ +package com.provectus.kafka.ui.exception; + +import java.nio.file.Path; + +public class FileUploadException extends CustomBaseException { + + public FileUploadException(String msg, Throwable cause) { + super(msg, cause); + } + + public FileUploadException(Path path, Throwable cause) { + super("Error uploading file %s".formatted(path), cause); + } + + @Override + public ErrorCode getErrorCode() { + return ErrorCode.FILE_UPLOAD_EXCEPTION; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ValidationException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ValidationException.java index 7b964fbca5..01eac145ff 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ValidationException.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ValidationException.java @@ -6,6 +6,10 @@ public class ValidationException extends CustomBaseException { super(message); } + public ValidationException(String message, Throwable cause) { + super(message, cause); + } + @Override public ErrorCode getErrorCode() { return ErrorCode.VALIDATION_FAIL; diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/ClusterMapper.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/ClusterMapper.java index ada0f6cf4e..b8721f6332 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/ClusterMapper.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/ClusterMapper.java @@ -6,12 +6,12 @@ import com.provectus.kafka.ui.model.BrokerDTO; import com.provectus.kafka.ui.model.BrokerDiskUsageDTO; import com.provectus.kafka.ui.model.BrokerMetricsDTO; import com.provectus.kafka.ui.model.ClusterDTO; +import com.provectus.kafka.ui.model.ClusterFeature; import com.provectus.kafka.ui.model.ClusterMetricsDTO; import com.provectus.kafka.ui.model.ClusterStatsDTO; import com.provectus.kafka.ui.model.ConfigSourceDTO; import com.provectus.kafka.ui.model.ConfigSynonymDTO; import com.provectus.kafka.ui.model.ConnectDTO; -import com.provectus.kafka.ui.model.Feature; import com.provectus.kafka.ui.model.InternalBroker; import com.provectus.kafka.ui.model.InternalBrokerConfig; import com.provectus.kafka.ui.model.InternalBrokerDiskUsage; @@ -103,7 +103,7 @@ public interface ClusterMapper { ConnectDTO toKafkaConnect(ClustersProperties.ConnectCluster connect); - List toFeaturesEnum(List features); + List toFeaturesEnum(List features); default List map(Map map) { return map.values().stream().map(this::toPartition).collect(Collectors.toList()); diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Feature.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/ClusterFeature.java similarity index 80% rename from kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Feature.java rename to kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/ClusterFeature.java index f35039190d..9ed7a38bb2 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Feature.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/ClusterFeature.java @@ -1,6 +1,6 @@ package com.provectus.kafka.ui.model; -public enum Feature { +public enum ClusterFeature { KAFKA_CONNECT, KSQL_DB, SCHEMA_REGISTRY, diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterState.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterState.java index 5f6d06ced5..28e9a7413a 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterState.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterState.java @@ -23,7 +23,7 @@ public class InternalClusterState { private Integer underReplicatedPartitionCount; private List diskUsage; private String version; - private List features; + private List features; private BigDecimal bytesInPerSec; private BigDecimal bytesOutPerSec; private Boolean readOnly; diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/JmxConnectionInfo.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/JmxConnectionInfo.java deleted file mode 100644 index de80b25be3..0000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/JmxConnectionInfo.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.provectus.kafka.ui.model; - -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; - -@Data -@RequiredArgsConstructor -@Builder -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -public class JmxConnectionInfo { - - @EqualsAndHashCode.Include - private final String url; - private final boolean ssl; - private final String username; - private final String password; - - public JmxConnectionInfo(String url) { - this.url = url; - this.ssl = false; - this.username = null; - this.password = null; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/KafkaCluster.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/KafkaCluster.java index d87113633f..9933d7e467 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/KafkaCluster.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/KafkaCluster.java @@ -26,7 +26,6 @@ public class KafkaCluster { private final String bootstrapServers; private final Properties properties; private final boolean readOnly; - private final boolean disableLogDirsCollection; private final MetricsConfig metricsConfig; private final DataMasking masking; private final Supplier throttler; diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/MetricsConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/MetricsConfig.java index 2554008080..d355144343 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/MetricsConfig.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/MetricsConfig.java @@ -17,4 +17,6 @@ public class MetricsConfig { private final boolean ssl; private final String username; private final String password; + private final String keystoreLocation; + private final String keystorePassword; } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Statistics.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Statistics.java index cb74c5d5ab..e70547f143 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Statistics.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Statistics.java @@ -15,7 +15,7 @@ public class Statistics { ServerStatusDTO status; Throwable lastKafkaException; String version; - List features; + List features; ReactiveAdminClient.ClusterDescription clusterDescription; Metrics metrics; InternalLogDirStats logDirInfo; diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/AccessContext.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/AccessContext.java index bed7747e8d..ff2f3f1e97 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/AccessContext.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/AccessContext.java @@ -1,5 +1,6 @@ package com.provectus.kafka.ui.model.rbac; +import com.provectus.kafka.ui.model.rbac.permission.ApplicationConfigAction; import com.provectus.kafka.ui.model.rbac.permission.AclAction; import com.provectus.kafka.ui.model.rbac.permission.ClusterConfigAction; import com.provectus.kafka.ui.model.rbac.permission.ConnectAction; @@ -16,6 +17,8 @@ import org.springframework.util.Assert; @Value public class AccessContext { + Collection applicationConfigActions; + String cluster; Collection clusterConfigActions; @@ -42,6 +45,7 @@ public class AccessContext { } public static final class AccessContextBuilder { + private Collection applicationConfigActions = Collections.emptySet(); private String cluster; private Collection clusterConfigActions = Collections.emptySet(); private String topic; @@ -59,6 +63,12 @@ public class AccessContext { private AccessContextBuilder() { } + public AccessContextBuilder applicationConfigActions(ApplicationConfigAction... actions) { + Assert.isTrue(actions.length > 0, "actions not present"); + this.applicationConfigActions = List.of(actions); + return this; + } + public AccessContextBuilder cluster(String cluster) { this.cluster = cluster; return this; @@ -132,7 +142,9 @@ public class AccessContext { } public AccessContext build() { - return new AccessContext(cluster, clusterConfigActions, + return new AccessContext( + applicationConfigActions, + cluster, clusterConfigActions, topic, topicActions, consumerGroup, consumerGroupActions, connect, connectActions, diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java index 9aa23b46c7..837f9008f3 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java @@ -3,6 +3,7 @@ package com.provectus.kafka.ui.model.rbac; import static com.provectus.kafka.ui.model.rbac.Resource.CLUSTERCONFIG; import static com.provectus.kafka.ui.model.rbac.Resource.KSQL; +import com.provectus.kafka.ui.model.rbac.permission.ApplicationConfigAction; import com.provectus.kafka.ui.model.rbac.permission.ClusterConfigAction; import com.provectus.kafka.ui.model.rbac.permission.ConnectAction; import com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction; @@ -12,11 +13,11 @@ import com.provectus.kafka.ui.model.rbac.permission.TopicAction; import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; +import javax.annotation.Nullable; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; import org.apache.commons.collections.CollectionUtils; -import org.jetbrains.annotations.Nullable; import org.springframework.util.Assert; @Getter @@ -25,18 +26,21 @@ import org.springframework.util.Assert; public class Permission { Resource resource; + List actions; @Nullable - Pattern value; - List actions; + String value; + @Nullable + transient Pattern compiledValuePattern; @SuppressWarnings("unused") public void setResource(String resource) { this.resource = Resource.fromString(resource.toUpperCase()); } - public void setValue(String value) { - this.value = Pattern.compile(value); + @SuppressWarnings("unused") + public void setValue(@Nullable String value) { + this.value = value; } @SuppressWarnings("unused") @@ -52,14 +56,17 @@ public class Permission { } public void transform() { - if (CollectionUtils.isEmpty(actions) || this.actions.stream().noneMatch("ALL"::equalsIgnoreCase)) { - return; + if (value != null) { + this.compiledValuePattern = Pattern.compile(value); + } + if (CollectionUtils.isNotEmpty(actions) && actions.stream().anyMatch("ALL"::equalsIgnoreCase)) { + this.actions = getAllActionValues(); } - this.actions = getActionValues(); } - private List getActionValues() { + private List getAllActionValues() { return switch (this.resource) { + case APPLICATIONCONFIG -> Arrays.stream(ApplicationConfigAction.values()).map(Enum::toString).toList(); case CLUSTERCONFIG -> Arrays.stream(ClusterConfigAction.values()).map(Enum::toString).toList(); case TOPIC -> Arrays.stream(TopicAction.values()).map(Enum::toString).toList(); case CONSUMER -> Arrays.stream(ConsumerGroupAction.values()).map(Enum::toString).toList(); diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Resource.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Resource.java index 4f8e30f208..f71dfb2979 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Resource.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Resource.java @@ -5,6 +5,7 @@ import org.jetbrains.annotations.Nullable; public enum Resource { + APPLICATIONCONFIG, CLUSTERCONFIG, TOPIC, CONSUMER, diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ApplicationConfigAction.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ApplicationConfigAction.java new file mode 100644 index 0000000000..d30ff50d70 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ApplicationConfigAction.java @@ -0,0 +1,18 @@ +package com.provectus.kafka.ui.model.rbac.permission; + +import org.apache.commons.lang3.EnumUtils; +import org.jetbrains.annotations.Nullable; + +public enum ApplicationConfigAction implements PermissibleAction { + + VIEW, + EDIT + + ; + + @Nullable + public static ApplicationConfigAction fromString(String name) { + return EnumUtils.getEnum(ApplicationConfigAction.class, name); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdesInitializer.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdesInitializer.java index b49767d1d5..40ea320b2e 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdesInitializer.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdesInitializer.java @@ -9,6 +9,7 @@ import com.provectus.kafka.ui.config.ClustersProperties.SerdeConfig; import com.provectus.kafka.ui.exception.ValidationException; import com.provectus.kafka.ui.serde.api.PropertyResolver; import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.builtin.AvroEmbeddedSerde; import com.provectus.kafka.ui.serdes.builtin.Base64Serde; import com.provectus.kafka.ui.serdes.builtin.Int32Serde; import com.provectus.kafka.ui.serdes.builtin.Int64Serde; @@ -43,6 +44,7 @@ public class SerdesInitializer { .put(Int64Serde.name(), Int64Serde.class) .put(UInt32Serde.name(), UInt32Serde.class) .put(UInt64Serde.name(), UInt64Serde.class) + .put(AvroEmbeddedSerde.name(), AvroEmbeddedSerde.class) .put(Base64Serde.name(), Base64Serde.class) .put(UuidBinarySerde.name(), UuidBinarySerde.class) .build(), @@ -87,21 +89,23 @@ public class SerdesInitializer { Map registeredSerdes = new LinkedHashMap<>(); // initializing serdes from config - for (int i = 0; i < clusterProperties.getSerde().size(); i++) { - SerdeConfig serdeConfig = clusterProperties.getSerde().get(i); - if (Strings.isNullOrEmpty(serdeConfig.getName())) { - throw new ValidationException("'name' property not set for serde: " + serdeConfig); + if (clusterProperties.getSerde() != null) { + for (int i = 0; i < clusterProperties.getSerde().size(); i++) { + SerdeConfig serdeConfig = clusterProperties.getSerde().get(i); + if (Strings.isNullOrEmpty(serdeConfig.getName())) { + throw new ValidationException("'name' property not set for serde: " + serdeConfig); + } + if (registeredSerdes.containsKey(serdeConfig.getName())) { + throw new ValidationException("Multiple serdes with same name: " + serdeConfig.getName()); + } + var instance = createSerdeFromConfig( + serdeConfig, + new PropertyResolverImpl(env, "kafka.clusters." + clusterIndex + ".serde." + i + ".properties"), + clusterPropertiesResolver, + globalPropertiesResolver + ); + registeredSerdes.put(serdeConfig.getName(), instance); } - if (registeredSerdes.containsKey(serdeConfig.getName())) { - throw new ValidationException("Multiple serdes with same name: " + serdeConfig.getName()); - } - var instance = createSerdeFromConfig( - serdeConfig, - new PropertyResolverImpl(env, "kafka.clusters." + clusterIndex + ".serde." + i + ".properties"), - clusterPropertiesResolver, - globalPropertiesResolver - ); - registeredSerdes.put(serdeConfig.getName(), instance); } // initializing remaining built-in serdes with empty selection patters @@ -170,7 +174,7 @@ public class SerdesInitializer { } var clazz = builtInSerdeClasses.get(name); BuiltInSerde serde = createSerdeInstance(clazz); - if (serdeConfig.getProperties().isEmpty()) { + if (serdeConfig.getProperties() == null || serdeConfig.getProperties().isEmpty()) { if (!autoConfigureSerde(serde, clusterProps, globalProps)) { // no properties provided and serde does not support auto-configuration throw new ValidationException(name + " serde is not configured"); diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/AvroEmbeddedSerde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/AvroEmbeddedSerde.java new file mode 100644 index 0000000000..73a1ed5484 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/AvroEmbeddedSerde.java @@ -0,0 +1,72 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.PropertyResolver; +import com.provectus.kafka.ui.serde.api.RecordHeaders; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serdes.BuiltInSerde; +import io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils; +import java.util.Map; +import java.util.Optional; +import lombok.SneakyThrows; +import org.apache.avro.file.DataFileReader; +import org.apache.avro.file.SeekableByteArrayInput; +import org.apache.avro.generic.GenericDatumReader; + +public class AvroEmbeddedSerde implements BuiltInSerde { + + public static String name() { + return "Avro (Embedded)"; + } + + @Override + public void configure(PropertyResolver serdeProperties, + PropertyResolver kafkaClusterProperties, + PropertyResolver globalProperties) { + } + + @Override + public Optional getDescription() { + return Optional.empty(); + } + + @Override + public Optional getSchema(String topic, Target type) { + return Optional.empty(); + } + + @Override + public boolean canDeserialize(String topic, Target type) { + return true; + } + + @Override + public boolean canSerialize(String topic, Target type) { + return false; + } + + @Override + public Serializer serializer(String topic, Target type) { + throw new IllegalStateException(); + } + + @Override + public Deserializer deserializer(String topic, Target type) { + return new Deserializer() { + @SneakyThrows + @Override + public DeserializeResult deserialize(RecordHeaders headers, byte[] data) { + try (var reader = new DataFileReader<>(new SeekableByteArrayInput(data), new GenericDatumReader<>())) { + if (!reader.hasNext()) { + // this is very strange situation, when only header present in payload + // returning null in this case + return new DeserializeResult(null, DeserializeResult.Type.JSON, Map.of()); + } + Object avroObj = reader.next(); + String jsonValue = new String(AvroSchemaUtils.toJson(avroObj)); + return new DeserializeResult(jsonValue, DeserializeResult.Type.JSON, Map.of()); + } + } + }; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerde.java index 664c75b70d..8a4c28a320 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerde.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerde.java @@ -1,9 +1,36 @@ package com.provectus.kafka.ui.serdes.builtin; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.protobuf.AnyProto; +import com.google.protobuf.ApiProto; +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Descriptors; import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.DurationProto; import com.google.protobuf.DynamicMessage; +import com.google.protobuf.EmptyProto; +import com.google.protobuf.FieldMaskProto; +import com.google.protobuf.SourceContextProto; +import com.google.protobuf.StructProto; +import com.google.protobuf.TimestampProto; +import com.google.protobuf.TypeProto; +import com.google.protobuf.WrappersProto; import com.google.protobuf.util.JsonFormat; +import com.google.type.ColorProto; +import com.google.type.DateProto; +import com.google.type.DateTimeProto; +import com.google.type.DayOfWeekProto; +import com.google.type.ExprProto; +import com.google.type.FractionProto; +import com.google.type.IntervalProto; +import com.google.type.LatLngProto; +import com.google.type.MoneyProto; +import com.google.type.MonthProto; +import com.google.type.PhoneNumberProto; +import com.google.type.PostalAddressProto; +import com.google.type.QuaternionProto; +import com.google.type.TimeOfDayProto; import com.provectus.kafka.ui.exception.ValidationException; import com.provectus.kafka.ui.serde.api.DeserializeResult; import com.provectus.kafka.ui.serde.api.PropertyResolver; @@ -11,13 +38,19 @@ import com.provectus.kafka.ui.serde.api.RecordHeaders; import com.provectus.kafka.ui.serde.api.SchemaDescription; import com.provectus.kafka.ui.serdes.BuiltInSerde; import com.provectus.kafka.ui.util.jsonschema.ProtobufSchemaConverter; +import com.squareup.wire.schema.ErrorCollector; +import com.squareup.wire.schema.Linker; +import com.squareup.wire.schema.Loader; +import com.squareup.wire.schema.Location; +import com.squareup.wire.schema.ProtoFile; +import com.squareup.wire.schema.internal.parser.ProtoFileElement; +import com.squareup.wire.schema.internal.parser.ProtoParser; import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchemaUtils; import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -28,7 +61,10 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +@Slf4j public class ProtobufFileSerde implements BuiltInSerde { public static String name() { @@ -51,132 +87,35 @@ public class ProtobufFileSerde implements BuiltInSerde { @Override public boolean canBeAutoConfigured(PropertyResolver kafkaClusterProperties, PropertyResolver globalProperties) { - Optional protobufFile = kafkaClusterProperties.getProperty("protobufFile", String.class); - Optional> protobufFiles = kafkaClusterProperties.getListProperty("protobufFiles", String.class); - return protobufFile.isPresent() || protobufFiles.filter(files -> !files.isEmpty()).isPresent(); + return Configuration.canBeAutoConfigured(kafkaClusterProperties); } @Override public void autoConfigure(PropertyResolver kafkaClusterProperties, PropertyResolver globalProperties) { - configure(kafkaClusterProperties); + configure(Configuration.create(kafkaClusterProperties)); } @Override public void configure(PropertyResolver serdeProperties, PropertyResolver kafkaClusterProperties, PropertyResolver globalProperties) { - configure(serdeProperties); - } - - private void configure(PropertyResolver properties) { - Map protobufSchemas = joinPathProperties(properties).stream() - .map(path -> Map.entry(path, new ProtobufSchema(readFileAsString(path)))) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - // Load all referenced message schemas and store their source proto file with the descriptors - Map descriptorPaths = new HashMap<>(); - Optional protobufMessageName = properties.getProperty("protobufMessageName", String.class); - protobufMessageName.ifPresent(messageName -> addProtobufSchema(descriptorPaths, protobufSchemas, messageName)); - - Optional protobufMessageNameForKey = - properties.getProperty("protobufMessageNameForKey", String.class); - protobufMessageNameForKey - .ifPresent(messageName -> addProtobufSchema(descriptorPaths, protobufSchemas, messageName)); - - Optional> protobufMessageNameByTopic = - properties.getMapProperty("protobufMessageNameByTopic", String.class, String.class); - protobufMessageNameByTopic - .ifPresent(messageNamesByTopic -> addProtobufSchemas(descriptorPaths, protobufSchemas, messageNamesByTopic)); - - Optional> protobufMessageNameForKeyByTopic = - properties.getMapProperty("protobufMessageNameForKeyByTopic", String.class, String.class); - protobufMessageNameForKeyByTopic - .ifPresent(messageNamesByTopic -> addProtobufSchemas(descriptorPaths, protobufSchemas, messageNamesByTopic)); - - // Fill dictionary for descriptor lookup by full message name - Map descriptorMap = descriptorPaths.keySet().stream() - .collect(Collectors.toMap(Descriptor::getFullName, Function.identity())); - - configure( - protobufMessageName.map(descriptorMap::get).orElse(null), - protobufMessageNameForKey.map(descriptorMap::get).orElse(null), - descriptorPaths, - protobufMessageNameByTopic.map(map -> populateDescriptors(descriptorMap, map)).orElse(Map.of()), - protobufMessageNameForKeyByTopic.map(map -> populateDescriptors(descriptorMap, map)).orElse(Map.of()) - ); + configure(Configuration.create(serdeProperties)); } @VisibleForTesting - void configure( - @Nullable Descriptor defaultMessageDescriptor, - @Nullable Descriptor defaultKeyMessageDescriptor, - Map descriptorPaths, - Map messageDescriptorMap, - Map keyMessageDescriptorMap) { - if (defaultMessageDescriptor == null - && defaultKeyMessageDescriptor == null - && messageDescriptorMap.isEmpty() - && keyMessageDescriptorMap.isEmpty()) { + void configure(Configuration configuration) { + if (configuration.defaultMessageDescriptor() == null + && configuration.defaultKeyMessageDescriptor() == null + && configuration.messageDescriptorMap().isEmpty() + && configuration.keyMessageDescriptorMap().isEmpty()) { throw new ValidationException("Neither default, not per-topic descriptors defined for " + name() + " serde"); } - this.defaultMessageDescriptor = defaultMessageDescriptor; - this.defaultKeyMessageDescriptor = defaultKeyMessageDescriptor; - this.descriptorPaths = descriptorPaths; - this.messageDescriptorMap = messageDescriptorMap; - this.keyMessageDescriptorMap = keyMessageDescriptorMap; - } - - private static void addProtobufSchema(Map descriptorPaths, - Map protobufSchemas, - String messageName) { - var descriptorAndPath = getDescriptorAndPath(protobufSchemas, messageName); - descriptorPaths.put(descriptorAndPath.getKey(), descriptorAndPath.getValue()); - } - - private static void addProtobufSchemas(Map descriptorPaths, - Map protobufSchemas, - Map messageNamesByTopic) { - messageNamesByTopic.values().stream() - .map(msgName -> getDescriptorAndPath(protobufSchemas, msgName)) - .forEach(entry -> descriptorPaths.put(entry.getKey(), entry.getValue())); - } - - private static List joinPathProperties(PropertyResolver propertyResolver) { - return Stream.concat( - propertyResolver.getProperty("protobufFile", String.class).map(List::of).stream(), - propertyResolver.getListProperty("protobufFiles", String.class).stream()) - .flatMap(Collection::stream) - .distinct() - .map(Path::of) - .collect(Collectors.toList()); - } - - private static Map.Entry getDescriptorAndPath(Map protobufSchemas, - String msgName) { - return protobufSchemas.entrySet().stream() - .filter(schema -> schema.getValue().toDescriptor(msgName) != null) - .map(schema -> Map.entry(schema.getValue().toDescriptor(msgName), schema.getKey())) - .findFirst() - .orElseThrow(() -> new NullPointerException( - "The given message type not found in protobuf definition: " + msgName)); - } - - private static String readFileAsString(Path path) { - try { - return Files.readString(path); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - private Map populateDescriptors(Map descriptorMap, - Map messageNameMap) { - Map descriptors = new HashMap<>(); - for (Map.Entry entry : messageNameMap.entrySet()) { - descriptors.put(entry.getKey(), descriptorMap.get(entry.getValue())); - } - return descriptors; + this.defaultMessageDescriptor = configuration.defaultMessageDescriptor(); + this.defaultKeyMessageDescriptor = configuration.defaultKeyMessageDescriptor(); + this.descriptorPaths = configuration.descriptorPaths(); + this.messageDescriptorMap = configuration.messageDescriptorMap(); + this.keyMessageDescriptorMap = configuration.keyMessageDescriptorMap(); } @Override @@ -249,4 +188,238 @@ public class ProtobufFileSerde implements BuiltInSerde { Map.of("messageName", descriptor.getFullName()) ); } + + @SneakyThrows + private static String readFileAsString(Path path) { + return Files.readString(path); + } + + //---------------------------------------------------------------------------------------------------------------- + + @VisibleForTesting + record Configuration(@Nullable Descriptor defaultMessageDescriptor, + @Nullable Descriptor defaultKeyMessageDescriptor, + Map descriptorPaths, + Map messageDescriptorMap, + Map keyMessageDescriptorMap) { + + static boolean canBeAutoConfigured(PropertyResolver kafkaClusterProperties) { + Optional protobufFile = kafkaClusterProperties.getProperty("protobufFile", String.class); + Optional> protobufFiles = kafkaClusterProperties.getListProperty("protobufFiles", String.class); + Optional protobufFilesDir = kafkaClusterProperties.getProperty("protobufFilesDir", String.class); + return protobufFilesDir.isPresent() + || protobufFile.isPresent() + || protobufFiles.filter(files -> !files.isEmpty()).isPresent(); + } + + static Configuration create(PropertyResolver properties) { + var protobufSchemas = loadSchemas( + properties.getProperty("protobufFile", String.class), + properties.getListProperty("protobufFiles", String.class), + properties.getProperty("protobufFilesDir", String.class) + ); + + // Load all referenced message schemas and store their source proto file with the descriptors + Map descriptorPaths = new HashMap<>(); + Optional protobufMessageName = properties.getProperty("protobufMessageName", String.class); + protobufMessageName.ifPresent(messageName -> addProtobufSchema(descriptorPaths, protobufSchemas, messageName)); + + Optional protobufMessageNameForKey = + properties.getProperty("protobufMessageNameForKey", String.class); + protobufMessageNameForKey + .ifPresent(messageName -> addProtobufSchema(descriptorPaths, protobufSchemas, messageName)); + + Optional> protobufMessageNameByTopic = + properties.getMapProperty("protobufMessageNameByTopic", String.class, String.class); + protobufMessageNameByTopic + .ifPresent(messageNamesByTopic -> addProtobufSchemas(descriptorPaths, protobufSchemas, messageNamesByTopic)); + + Optional> protobufMessageNameForKeyByTopic = + properties.getMapProperty("protobufMessageNameForKeyByTopic", String.class, String.class); + protobufMessageNameForKeyByTopic + .ifPresent(messageNamesByTopic -> addProtobufSchemas(descriptorPaths, protobufSchemas, messageNamesByTopic)); + + // Fill dictionary for descriptor lookup by full message name + Map descriptorMap = descriptorPaths.keySet().stream() + .collect(Collectors.toMap(Descriptor::getFullName, Function.identity())); + + return new Configuration( + protobufMessageName.map(descriptorMap::get).orElse(null), + protobufMessageNameForKey.map(descriptorMap::get).orElse(null), + descriptorPaths, + protobufMessageNameByTopic.map(map -> populateDescriptors(descriptorMap, map)).orElse(Map.of()), + protobufMessageNameForKeyByTopic.map(map -> populateDescriptors(descriptorMap, map)).orElse(Map.of()) + ); + } + + private static Map.Entry getDescriptorAndPath(Map protobufSchemas, + String msgName) { + return protobufSchemas.entrySet().stream() + .filter(schema -> schema.getValue().toDescriptor(msgName) != null) + .map(schema -> Map.entry(schema.getValue().toDescriptor(msgName), schema.getKey())) + .findFirst() + .orElseThrow(() -> new NullPointerException( + "The given message type not found in protobuf definition: " + msgName)); + } + + private static Map populateDescriptors(Map descriptorMap, + Map messageNameMap) { + Map descriptors = new HashMap<>(); + for (Map.Entry entry : messageNameMap.entrySet()) { + descriptors.put(entry.getKey(), descriptorMap.get(entry.getValue())); + } + return descriptors; + } + + @VisibleForTesting + static Map loadSchemas(Optional protobufFile, + Optional> protobufFiles, + Optional protobufFilesDir) { + if (protobufFilesDir.isPresent()) { + if (protobufFile.isPresent() || protobufFiles.isPresent()) { + log.warn("protobufFile and protobufFiles properties will be ignored, since protobufFilesDir provided"); + } + List loadedFiles = new ProtoSchemaLoader(protobufFilesDir.get()).load(); + Map allPaths = loadedFiles.stream() + .collect(Collectors.toMap(f -> f.getLocation().getPath(), ProtoFile::toElement)); + return loadedFiles.stream() + .collect(Collectors.toMap( + f -> Path.of(f.getLocation().getBase(), f.getLocation().getPath()), + f -> new ProtobufSchema(f.toElement(), List.of(), allPaths))); + } + //Supporting for backward-compatibility. Normally, protobufFilesDir setting should be used + return Stream.concat( + protobufFile.stream(), + protobufFiles.stream().flatMap(Collection::stream) + ) + .distinct() + .map(Path::of) + .collect(Collectors.toMap(path -> path, path -> new ProtobufSchema(readFileAsString(path)))); + } + + private static void addProtobufSchema(Map descriptorPaths, + Map protobufSchemas, + String messageName) { + var descriptorAndPath = getDescriptorAndPath(protobufSchemas, messageName); + descriptorPaths.put(descriptorAndPath.getKey(), descriptorAndPath.getValue()); + } + + private static void addProtobufSchemas(Map descriptorPaths, + Map protobufSchemas, + Map messageNamesByTopic) { + messageNamesByTopic.values().stream() + .map(msgName -> getDescriptorAndPath(protobufSchemas, msgName)) + .forEach(entry -> descriptorPaths.put(entry.getKey(), entry.getValue())); + } + } + + static class ProtoSchemaLoader { + + private final Path baseLocation; + + ProtoSchemaLoader(String baseLocationStr) { + this.baseLocation = Path.of(baseLocationStr); + if (!Files.isReadable(baseLocation)) { + throw new ValidationException("proto files directory not readable"); + } + } + + List load() { + Map knownTypes = knownProtoFiles(); + + Map filesByLocations = new HashMap<>(); + filesByLocations.putAll(knownTypes); + filesByLocations.putAll(loadFilesWithLocations()); + + Linker linker = new Linker( + createFilesLoader(filesByLocations), + new ErrorCollector(), + true, + true + ); + var schema = linker.link(filesByLocations.values()); + linker.getErrors().throwIfNonEmpty(); + return schema.getProtoFiles() + .stream() + .filter(p -> !knownTypes.containsKey(p.getLocation().getPath())) //filtering known types + .toList(); + } + + private Map knownProtoFiles() { + return Stream.of( + loadKnownProtoFile("google/type/color.proto", ColorProto.getDescriptor()), + loadKnownProtoFile("google/type/date.proto", DateProto.getDescriptor()), + loadKnownProtoFile("google/type/datetime.proto", DateTimeProto.getDescriptor()), + loadKnownProtoFile("google/type/dayofweek.proto", DayOfWeekProto.getDescriptor()), + loadKnownProtoFile("google/type/decimal.proto", com.google.type.DecimalProto.getDescriptor()), + loadKnownProtoFile("google/type/expr.proto", ExprProto.getDescriptor()), + loadKnownProtoFile("google/type/fraction.proto", FractionProto.getDescriptor()), + loadKnownProtoFile("google/type/interval.proto", IntervalProto.getDescriptor()), + loadKnownProtoFile("google/type/latlng.proto", LatLngProto.getDescriptor()), + loadKnownProtoFile("google/type/money.proto", MoneyProto.getDescriptor()), + loadKnownProtoFile("google/type/month.proto", MonthProto.getDescriptor()), + loadKnownProtoFile("google/type/phone_number.proto", PhoneNumberProto.getDescriptor()), + loadKnownProtoFile("google/type/postal_address.proto", PostalAddressProto.getDescriptor()), + loadKnownProtoFile("google/type/quaternion.prot", QuaternionProto.getDescriptor()), + loadKnownProtoFile("google/type/timeofday.proto", TimeOfDayProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/any.proto", AnyProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/api.proto", ApiProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/descriptor.proto", DescriptorProtos.getDescriptor()), + loadKnownProtoFile("google/protobuf/duration.proto", DurationProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/empty.proto", EmptyProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/field_mask.proto", FieldMaskProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/source_context.proto", SourceContextProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/struct.proto", StructProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/timestamp.proto", TimestampProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/type.proto", TypeProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/wrappers.proto", WrappersProto.getDescriptor()) + ).collect(Collectors.toMap(p -> p.getLocation().getPath(), p -> p)); + } + + private ProtoFile loadKnownProtoFile(String path, Descriptors.FileDescriptor fileDescriptor) { + String protoFileString = null; + // know type file contains either message or enum + if (!fileDescriptor.getMessageTypes().isEmpty()) { + protoFileString = new ProtobufSchema(fileDescriptor.getMessageTypes().get(0)).canonicalString(); + } else if (!fileDescriptor.getEnumTypes().isEmpty()) { + protoFileString = new ProtobufSchema(fileDescriptor.getEnumTypes().get(0)).canonicalString(); + } else { + throw new IllegalStateException(); + } + return ProtoFile.Companion.get(ProtoParser.Companion.parse(Location.get(path), protoFileString)); + } + + private Loader createFilesLoader(Map files) { + return new Loader() { + @Override + public @NotNull ProtoFile load(@NotNull String path) { + return Preconditions.checkNotNull(files.get(path), "ProtoFile not found for import '%s'", path); + } + + @Override + public @NotNull Loader withErrors(@NotNull ErrorCollector errorCollector) { + return this; + } + }; + } + + @SneakyThrows + private Map loadFilesWithLocations() { + Map filesByLocations = new HashMap<>(); + try (var files = Files.walk(baseLocation)) { + files.filter(p -> !Files.isDirectory(p) && p.toString().endsWith(".proto")) + .forEach(path -> { + // relative path will be used as "import" statement + String relativePath = baseLocation.relativize(path).toString(); + var protoFileElement = ProtoParser.Companion.parse( + Location.get(baseLocation.toString(), relativePath), + readFileAsString(path) + ); + filesByLocations.put(relativePath, ProtoFile.Companion.get(protoFileElement)); + }); + } + return filesByLocations; + } + } + } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaRegistrySerde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaRegistrySerde.java index a4d7ee8891..fd5985e20c 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaRegistrySerde.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaRegistrySerde.java @@ -70,10 +70,10 @@ public class SchemaRegistrySerde implements BuiltInSerde { urls, kafkaClusterProperties.getProperty("schemaRegistryAuth.username", String.class).orElse(null), kafkaClusterProperties.getProperty("schemaRegistryAuth.password", String.class).orElse(null), - kafkaClusterProperties.getProperty("schemaRegistrySSL.keystoreLocation", String.class).orElse(null), - kafkaClusterProperties.getProperty("schemaRegistrySSL.keystorePassword", String.class).orElse(null), - kafkaClusterProperties.getProperty("schemaRegistrySSL.truststoreLocation", String.class).orElse(null), - kafkaClusterProperties.getProperty("schemaRegistrySSL.truststorePassword", String.class).orElse(null) + kafkaClusterProperties.getProperty("schemaRegistrySsl.keystoreLocation", String.class).orElse(null), + kafkaClusterProperties.getProperty("schemaRegistrySsl.keystorePassword", String.class).orElse(null), + kafkaClusterProperties.getProperty("ssl.truststoreLocation", String.class).orElse(null), + kafkaClusterProperties.getProperty("ssl.truststorePassword", String.class).orElse(null) ), kafkaClusterProperties.getProperty("schemaRegistryKeySchemaNameTemplate", String.class).orElse("%s-key"), kafkaClusterProperties.getProperty("schemaRegistrySchemaNameTemplate", String.class).orElse("%s-value"), @@ -98,12 +98,12 @@ public class SchemaRegistrySerde implements BuiltInSerde { serdeProperties.getProperty("password", String.class).orElse(null), serdeProperties.getProperty("keystoreLocation", String.class).orElse(null), serdeProperties.getProperty("keystorePassword", String.class).orElse(null), - serdeProperties.getProperty("truststoreLocation", String.class).orElse(null), - serdeProperties.getProperty("truststorePassword", String.class).orElse(null) + kafkaClusterProperties.getProperty("ssl.truststoreLocation", String.class).orElse(null), + kafkaClusterProperties.getProperty("ssl.truststorePassword", String.class).orElse(null) ), serdeProperties.getProperty("keySchemaNameTemplate", String.class).orElse("%s-key"), serdeProperties.getProperty("schemaNameTemplate", String.class).orElse("%s-value"), - kafkaClusterProperties.getProperty("checkSchemaExistenceForDeserialize", Boolean.class) + serdeProperties.getProperty("checkSchemaExistenceForDeserialize", Boolean.class) .orElse(false) ); } @@ -148,15 +148,15 @@ public class SchemaRegistrySerde implements BuiltInSerde { trustStoreLocation); configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, trustStorePassword); + } - if (keyStoreLocation != null) { - configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, - keyStoreLocation); - configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, - keyStorePassword); - configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_KEY_PASSWORD_CONFIG, - keyStorePassword); - } + if (keyStoreLocation != null && keyStorePassword != null) { + configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, + keyStoreLocation); + configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, + keyStorePassword); + configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_KEY_PASSWORD_CONFIG, + keyStorePassword); } return new CachedSchemaRegistryClient( diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/AdminClientServiceImpl.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/AdminClientServiceImpl.java index 3589a07a47..886b67b928 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/AdminClientServiceImpl.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/AdminClientServiceImpl.java @@ -1,10 +1,13 @@ package com.provectus.kafka.ui.service; import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.util.SslPropertiesUtil; import java.io.Closeable; +import java.time.Instant; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -18,6 +21,9 @@ import reactor.core.publisher.Mono; @RequiredArgsConstructor @Slf4j public class AdminClientServiceImpl implements AdminClientService, Closeable { + + private static final AtomicLong CLIENT_ID_SEQ = new AtomicLong(); + private final Map adminClientCache = new ConcurrentHashMap<>(); @Setter // used in tests @Value("${kafka.admin-client-timeout:30000}") @@ -33,14 +39,16 @@ public class AdminClientServiceImpl implements AdminClientService, Closeable { private Mono createAdminClient(KafkaCluster cluster) { return Mono.fromSupplier(() -> { Properties properties = new Properties(); + SslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), properties); properties.putAll(cluster.getProperties()); - properties - .put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); + properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); properties.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, clientTimeout); - properties.putIfAbsent(AdminClientConfig.CLIENT_ID_CONFIG, "kafka-ui-admin-client-" + System.currentTimeMillis()); + properties.putIfAbsent( + AdminClientConfig.CLIENT_ID_CONFIG, + "kafka-ui-admin-" + Instant.now().getEpochSecond() + "-" + CLIENT_ID_SEQ.incrementAndGet() + ); return AdminClient.create(properties); - }) - .flatMap(ReactiveAdminClient::create) + }).flatMap(ac -> ReactiveAdminClient.create(ac).doOnError(th -> ac.close())) .onErrorMap(th -> new IllegalStateException( "Error while creating AdminClient for Cluster " + cluster.getName(), th)); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ConsumerGroupService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ConsumerGroupService.java index 1a74914ff4..024eb3df51 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ConsumerGroupService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ConsumerGroupService.java @@ -7,6 +7,7 @@ import com.provectus.kafka.ui.model.InternalTopicConsumerGroup; import com.provectus.kafka.ui.model.KafkaCluster; import com.provectus.kafka.ui.model.SortOrderDTO; import com.provectus.kafka.ui.service.rbac.AccessControlService; +import com.provectus.kafka.ui.util.SslPropertiesUtil; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; @@ -214,6 +215,7 @@ public class ConsumerGroupService { public KafkaConsumer createConsumer(KafkaCluster cluster, Map properties) { Properties props = new Properties(); + SslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), props); props.putAll(cluster.getProperties()); props.put(ConsumerConfig.CLIENT_ID_CONFIG, "kafka-ui-consumer-" + System.currentTimeMillis()); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/FeatureService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/FeatureService.java index 1241de4347..e08b5a746a 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/FeatureService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/FeatureService.java @@ -1,8 +1,7 @@ package com.provectus.kafka.ui.service; -import com.provectus.kafka.ui.model.Feature; +import com.provectus.kafka.ui.model.ClusterFeature; import com.provectus.kafka.ui.model.KafkaCluster; -import com.provectus.kafka.ui.service.ReactiveAdminClient.SupportedFeature; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -26,25 +25,28 @@ public class FeatureService { private final AdminClientService adminClientService; - public Mono> getAvailableFeatures(KafkaCluster cluster, @Nullable Node controller) { - List> features = new ArrayList<>(); + public Mono> getAvailableFeatures(KafkaCluster cluster, @Nullable Node controller) { + List> features = new ArrayList<>(); if (Optional.ofNullable(cluster.getConnectsClients()) .filter(Predicate.not(Map::isEmpty)) .isPresent()) { - features.add(Mono.just(Feature.KAFKA_CONNECT)); + features.add(Mono.just(ClusterFeature.KAFKA_CONNECT)); } if (cluster.getKsqlClient() != null) { - features.add(Mono.just(Feature.KSQL_DB)); + features.add(Mono.just(ClusterFeature.KSQL_DB)); } if (cluster.getSchemaRegistryClient() != null) { - features.add(Mono.just(Feature.SCHEMA_REGISTRY)); + features.add(Mono.just(ClusterFeature.SCHEMA_REGISTRY)); } if (controller != null) { - features.add(topicDeletion(cluster, controller)); + features.add( + isTopicDeletionEnabled(cluster, controller) + .flatMap(r -> Boolean.TRUE.equals(r) ? Mono.just(ClusterFeature.TOPIC_DELETION) : Mono.empty()) + ); } features.add(acl(cluster)); @@ -52,7 +54,7 @@ public class FeatureService { return Flux.fromIterable(features).flatMap(m -> m).collectList(); } - private Mono topicDeletion(KafkaCluster cluster, Node controller) { + private Mono isTopicDeletionEnabled(KafkaCluster cluster, Node controller) { return adminClientService.get(cluster) .flatMap(ac -> ac.loadBrokersConfig(List.of(controller.id()))) .map(config -> @@ -61,14 +63,13 @@ public class FeatureService { .filter(e -> e.name().equals(DELETE_TOPIC_ENABLED_SERVER_PROPERTY)) .map(e -> Boolean.parseBoolean(e.value())) .findFirst() - .orElse(true)) - .flatMap(enabled -> enabled ? Mono.just(Feature.TOPIC_DELETION) : Mono.empty()); + .orElse(true)); } - private Mono acl(KafkaCluster cluster) { + private Mono acl(KafkaCluster cluster) { return adminClientService.get(cluster).flatMap( ac -> ac.getClusterFeatures().contains(SupportedFeature.AUTHORIZED_SECURITY_ENABLED) - ? Mono.just(Feature.KAFKA_ACL) + ? Mono.just(ClusterFeature.KAFKA_ACL) : Mono.empty() ); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaClusterFactory.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaClusterFactory.java index b1000131ee..26a9d40647 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaClusterFactory.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaClusterFactory.java @@ -3,12 +3,15 @@ package com.provectus.kafka.ui.service; import com.provectus.kafka.ui.client.RetryingKafkaConnectClient; import com.provectus.kafka.ui.config.ClustersProperties; import com.provectus.kafka.ui.connect.api.KafkaConnectClientApi; +import com.provectus.kafka.ui.model.ApplicationPropertyValidationDTO; +import com.provectus.kafka.ui.model.ClusterConfigValidationDTO; import com.provectus.kafka.ui.model.KafkaCluster; import com.provectus.kafka.ui.model.MetricsConfig; import com.provectus.kafka.ui.service.ksql.KsqlApiClient; import com.provectus.kafka.ui.service.masking.DataMasking; import com.provectus.kafka.ui.sr.ApiClient; import com.provectus.kafka.ui.sr.api.KafkaSrClientApi; +import com.provectus.kafka.ui.util.KafkaServicesValidation; import com.provectus.kafka.ui.util.PollingThrottler; import com.provectus.kafka.ui.util.ReactiveFailover; import com.provectus.kafka.ui.util.WebClientConfigurator; @@ -20,13 +23,19 @@ import java.util.Properties; import java.util.stream.Stream; import javax.annotation.Nullable; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.util.unit.DataSize; import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; @Service @RequiredArgsConstructor +@Slf4j public class KafkaClusterFactory { @Value("${webclient.max-in-memory-buffer-size:20MB}") @@ -37,52 +46,118 @@ public class KafkaClusterFactory { builder.name(clusterProperties.getName()); builder.bootstrapServers(clusterProperties.getBootstrapServers()); - builder.properties(Optional.ofNullable(clusterProperties.getProperties()).orElse(new Properties())); + builder.properties(convertProperties(clusterProperties.getProperties())); builder.readOnly(clusterProperties.isReadOnly()); - builder.disableLogDirsCollection(clusterProperties.isDisableLogDirsCollection()); builder.masking(DataMasking.create(clusterProperties.getMasking())); - builder.metricsConfig(metricsConfigDataToMetricsConfig(clusterProperties.getMetrics())); builder.throttler(PollingThrottler.throttlerSupplier(clusterProperties)); - builder.schemaRegistryClient(schemaRegistryClient(clusterProperties)); - builder.connectsClients(connectClients(clusterProperties)); - builder.ksqlClient(ksqlClient(clusterProperties)); - + if (schemaRegistryConfigured(clusterProperties)) { + builder.schemaRegistryClient(schemaRegistryClient(clusterProperties)); + } + if (connectClientsConfigured(clusterProperties)) { + builder.connectsClients(connectClients(clusterProperties)); + } + if (ksqlConfigured(clusterProperties)) { + builder.ksqlClient(ksqlClient(clusterProperties)); + } + if (metricsConfigured(clusterProperties)) { + builder.metricsConfig(metricsConfigDataToMetricsConfig(clusterProperties.getMetrics())); + } builder.originalProperties(clusterProperties); - return builder.build(); } - @Nullable + public Mono validate(ClustersProperties.Cluster clusterProperties) { + if (clusterProperties.getSsl() != null) { + Optional errMsg = KafkaServicesValidation.validateTruststore(clusterProperties.getSsl()); + if (errMsg.isPresent()) { + return Mono.just(new ClusterConfigValidationDTO() + .kafka(new ApplicationPropertyValidationDTO() + .error(true) + .errorMessage("Truststore not valid: " + errMsg.get()))); + } + } + + return Mono.zip( + KafkaServicesValidation.validateClusterConnection( + clusterProperties.getBootstrapServers(), + convertProperties(clusterProperties.getProperties()), + clusterProperties.getSsl() + ), + schemaRegistryConfigured(clusterProperties) + ? KafkaServicesValidation.validateSchemaRegistry( + () -> schemaRegistryClient(clusterProperties)).map(Optional::of) + : Mono.>just(Optional.empty()), + + ksqlConfigured(clusterProperties) + ? KafkaServicesValidation.validateKsql(() -> ksqlClient(clusterProperties)).map(Optional::of) + : Mono.>just(Optional.empty()), + + connectClientsConfigured(clusterProperties) + ? + Flux.fromIterable(clusterProperties.getKafkaConnect()) + .flatMap(c -> + KafkaServicesValidation.validateConnect(() -> connectClient(clusterProperties, c)) + .map(r -> Tuples.of(c.getName(), r))) + .collectMap(Tuple2::getT1, Tuple2::getT2) + .map(Optional::of) + : + Mono.>>just(Optional.empty()) + ).map(tuple -> { + var validation = new ClusterConfigValidationDTO(); + validation.kafka(tuple.getT1()); + tuple.getT2().ifPresent(validation::schemaRegistry); + tuple.getT3().ifPresent(validation::ksqldb); + tuple.getT4().ifPresent(validation::kafkaConnects); + return validation; + }); + } + + private Properties convertProperties(Map propertiesMap) { + Properties properties = new Properties(); + if (propertiesMap != null) { + properties.putAll(propertiesMap); + } + return properties; + } + + private boolean connectClientsConfigured(ClustersProperties.Cluster clusterProperties) { + return clusterProperties.getKafkaConnect() != null; + } + private Map> connectClients( ClustersProperties.Cluster clusterProperties) { - if (clusterProperties.getKafkaConnect() == null) { - return null; - } Map> connects = new HashMap<>(); - clusterProperties.getKafkaConnect().forEach(c -> { - ReactiveFailover failover = ReactiveFailover.create( - parseUrlList(c.getAddress()), - url -> new RetryingKafkaConnectClient(c.toBuilder().address(url).build(), maxBuffSize), - ReactiveFailover.CONNECTION_REFUSED_EXCEPTION_FILTER, - "No alive connect instances available", - ReactiveFailover.DEFAULT_RETRY_GRACE_PERIOD_MS - ); - connects.put(c.getName(), failover); - }); + clusterProperties.getKafkaConnect().forEach(c -> connects.put(c.getName(), connectClient(clusterProperties, c))); return connects; } - @Nullable + private ReactiveFailover connectClient(ClustersProperties.Cluster cluster, + ClustersProperties.ConnectCluster connectCluster) { + return ReactiveFailover.create( + parseUrlList(connectCluster.getAddress()), + url -> new RetryingKafkaConnectClient( + connectCluster.toBuilder().address(url).build(), + cluster.getSsl(), + maxBuffSize + ), + ReactiveFailover.CONNECTION_REFUSED_EXCEPTION_FILTER, + "No alive connect instances available", + ReactiveFailover.DEFAULT_RETRY_GRACE_PERIOD_MS + ); + } + + private boolean schemaRegistryConfigured(ClustersProperties.Cluster clusterProperties) { + return clusterProperties.getSchemaRegistry() != null; + } + private ReactiveFailover schemaRegistryClient(ClustersProperties.Cluster clusterProperties) { - if (clusterProperties.getSchemaRegistry() == null) { - return null; - } var auth = Optional.ofNullable(clusterProperties.getSchemaRegistryAuth()) .orElse(new ClustersProperties.SchemaRegistryAuth()); WebClient webClient = new WebClientConfigurator() - .configureSsl(clusterProperties.getSchemaRegistrySsl()) + .configureSsl(clusterProperties.getSsl(), clusterProperties.getSchemaRegistrySsl()) .configureBasicAuth(auth.getUsername(), auth.getPassword()) + .configureBufferSize(maxBuffSize) .build(); return ReactiveFailover.create( parseUrlList(clusterProperties.getSchemaRegistry()), @@ -93,16 +168,17 @@ public class KafkaClusterFactory { ); } - @Nullable + private boolean ksqlConfigured(ClustersProperties.Cluster clusterProperties) { + return clusterProperties.getKsqldbServer() != null; + } + private ReactiveFailover ksqlClient(ClustersProperties.Cluster clusterProperties) { - if (clusterProperties.getKsqldbServer() == null) { - return null; - } return ReactiveFailover.create( parseUrlList(clusterProperties.getKsqldbServer()), url -> new KsqlApiClient( url, clusterProperties.getKsqldbServerAuth(), + clusterProperties.getSsl(), clusterProperties.getKsqldbServerSsl(), maxBuffSize ), @@ -116,6 +192,10 @@ public class KafkaClusterFactory { return Stream.of(url.split(",")).map(String::trim).filter(s -> !s.isBlank()).toList(); } + private boolean metricsConfigured(ClustersProperties.Cluster clusterProperties) { + return clusterProperties.getMetrics() != null; + } + @Nullable private MetricsConfig metricsConfigDataToMetricsConfig(ClustersProperties.MetricsConfigData metricsConfigData) { if (metricsConfigData == null) { @@ -124,9 +204,11 @@ public class KafkaClusterFactory { MetricsConfig.MetricsConfigBuilder builder = MetricsConfig.builder(); builder.type(metricsConfigData.getType()); builder.port(metricsConfigData.getPort()); - builder.ssl(metricsConfigData.isSsl()); + builder.ssl(Optional.ofNullable(metricsConfigData.getSsl()).orElse(false)); builder.username(metricsConfigData.getUsername()); builder.password(metricsConfigData.getPassword()); + builder.keystoreLocation(metricsConfigData.getKeystoreLocation()); + builder.keystorePassword(metricsConfigData.getKeystorePassword()); return builder.build(); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConfigSanitizer.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConfigSanitizer.java index 30daa1ca57..aa26709822 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConfigSanitizer.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConfigSanitizer.java @@ -17,7 +17,8 @@ import org.springframework.stereotype.Component; class KafkaConfigSanitizer extends Sanitizer { private static final List DEFAULT_PATTERNS_TO_SANITIZE = Arrays.asList( "basic.auth.user.info", /* For Schema Registry credentials */ - "password", "secret", "token", "key", ".*credentials.*" /* General credential patterns */ + "password", "secret", "token", "key", ".*credentials.*", /* General credential patterns */ + "aws.access.*", "aws.secret.*", "aws.session.*" /* AWS-related credential patterns */ ); KafkaConfigSanitizer( diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java index cfc190cffd..163732fae9 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java @@ -28,10 +28,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.Nullable; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -40,7 +40,6 @@ import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.util.function.Tuple2; import reactor.util.function.Tuples; @Service @@ -52,18 +51,18 @@ public class KafkaConnectService { private final ObjectMapper objectMapper; private final KafkaConfigSanitizer kafkaConfigSanitizer; - public List getConnects(KafkaCluster cluster) { - return Optional.ofNullable(cluster.getOriginalProperties().getKafkaConnect()) - .map(lst -> lst.stream().map(clusterMapper::toKafkaConnect).toList()) - .orElse(List.of()); + public Flux getConnects(KafkaCluster cluster) { + return Flux.fromIterable( + Optional.ofNullable(cluster.getOriginalProperties().getKafkaConnect()) + .map(lst -> lst.stream().map(clusterMapper::toKafkaConnect).toList()) + .orElse(List.of()) + ); } public Flux getAllConnectors(final KafkaCluster cluster, - final String search) { - Mono> clusters = Mono.just(Flux.fromIterable(getConnects(cluster))); // TODO get rid - return clusters - .flatMapMany(Function.identity()) - .flatMap(connect -> getConnectorNames(cluster, connect.getName())) + @Nullable final String search) { + return getConnects(cluster) + .flatMap(connect -> getConnectorNames(cluster, connect.getName()).map(cn -> Tuples.of(connect.getName(), cn))) .flatMap(pair -> getConnector(cluster, pair.getT1(), pair.getT2())) .flatMap(connector -> getConnectorConfig(cluster, connector.getConnect(), connector.getName()) @@ -99,56 +98,46 @@ public class KafkaConnectService { .filter(matchesSearchTerm(search)); } - private Predicate matchesSearchTerm(final String search) { - return connector -> getSearchValues(connector) - .anyMatch(value -> value.contains( - StringUtils.defaultString( - search, - StringUtils.EMPTY) - .toUpperCase())); + private Predicate matchesSearchTerm(@Nullable final String search) { + if (search == null) { + return c -> true; + } + return connector -> getStringsForSearch(connector) + .anyMatch(string -> StringUtils.containsIgnoreCase(string, search)); } - private Stream getSearchValues(FullConnectorInfoDTO fullConnectorInfo) { + private Stream getStringsForSearch(FullConnectorInfoDTO fullConnectorInfo) { return Stream.of( - fullConnectorInfo.getName(), - fullConnectorInfo.getStatus().getState().getValue(), - fullConnectorInfo.getType().getValue()) - .map(String::toUpperCase); + fullConnectorInfo.getName(), + fullConnectorInfo.getStatus().getState().getValue(), + fullConnectorInfo.getType().getValue()); } - private Mono getConnectorTopics(KafkaCluster cluster, String connectClusterName, - String connectorName) { + public Mono getConnectorTopics(KafkaCluster cluster, String connectClusterName, + String connectorName) { return api(cluster, connectClusterName) .mono(c -> c.getConnectorTopics(connectorName)) .map(result -> result.get(connectorName)) - // old connectors don't have this api, setting empty list for + // old Connect API versions don't have this endpoint, setting empty list for // backward-compatibility .onErrorResume(Exception.class, e -> Mono.just(new ConnectorTopics().topics(List.of()))); } - private Flux> getConnectorNames(KafkaCluster cluster, String connectName) { - return getConnectors(cluster, connectName) - .collectList().map(e -> e.get(0)) + public Flux getConnectorNames(KafkaCluster cluster, String connectName) { + return api(cluster, connectName) + .flux(client -> client.getConnectors(null)) // for some reason `getConnectors` method returns the response as a single string - .map(this::parseToList) - .flatMapMany(Flux::fromIterable) - .map(connector -> Tuples.of(connectName, connector)); + .collectList().map(e -> e.get(0)) + .map(this::parseConnectorsNamesStringToList) + .flatMapMany(Flux::fromIterable); } @SneakyThrows - private List parseToList(String json) { + private List parseConnectorsNamesStringToList(String json) { return objectMapper.readValue(json, new TypeReference<>() { }); } - public Flux getConnectors(KafkaCluster cluster, String connectName) { - return api(cluster, connectName) - .flux(client -> - client.getConnectors(null) - .doOnError(e -> log.error("Unexpected error upon getting connectors", e)) - ); - } - public Mono createConnector(KafkaCluster cluster, String connectName, Mono connector) { return api(cluster, connectName) @@ -171,9 +160,7 @@ public class KafkaConnectService { private Mono connectorExists(KafkaCluster cluster, String connectName, String connectorName) { return getConnectorNames(cluster, connectName) - .map(Tuple2::getT2) - .collectList() - .map(connectorNames -> connectorNames.contains(connectorName)); + .any(name -> name.equals(connectorName)); } public Mono getConnector(KafkaCluster cluster, String connectName, diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MessagesService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MessagesService.java index eaf6fbb889..d1f0e261a8 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MessagesService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MessagesService.java @@ -18,6 +18,7 @@ import com.provectus.kafka.ui.serde.api.Serde; import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer; import com.provectus.kafka.ui.serdes.ProducerRecordCreator; import com.provectus.kafka.ui.util.ResultSizeLimiter; +import com.provectus.kafka.ui.util.SslPropertiesUtil; import java.util.List; import java.util.Map; import java.util.Properties; @@ -108,6 +109,7 @@ public class MessagesService { ); Properties properties = new Properties(); + SslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), properties); properties.putAll(cluster.getProperties()); properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class); diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java index 6c2ef63091..e8bca34807 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java @@ -11,7 +11,7 @@ import com.google.common.collect.Table; import com.provectus.kafka.ui.exception.IllegalEntityStateException; import com.provectus.kafka.ui.exception.NotFoundException; import com.provectus.kafka.ui.exception.ValidationException; -import com.provectus.kafka.ui.util.NumberUtil; +import com.provectus.kafka.ui.util.KafkaVersion; import com.provectus.kafka.ui.util.annotation.KafkaClientInternalsDependant; import java.io.Closeable; import java.util.ArrayList; @@ -64,10 +64,12 @@ import org.apache.kafka.common.acl.AclBinding; import org.apache.kafka.common.acl.AclBindingFilter; import org.apache.kafka.common.acl.AclOperation; import org.apache.kafka.common.config.ConfigResource; +import org.apache.kafka.common.errors.ClusterAuthorizationException; import org.apache.kafka.common.errors.GroupIdNotFoundException; import org.apache.kafka.common.errors.GroupNotEmptyException; import org.apache.kafka.common.errors.InvalidRequestException; import org.apache.kafka.common.errors.SecurityDisabledException; +import org.apache.kafka.common.errors.TopicAuthorizationException; import org.apache.kafka.common.errors.UnknownTopicOrPartitionException; import org.apache.kafka.common.errors.UnsupportedVersionException; import org.apache.kafka.common.requests.DescribeLogDirsResponse; @@ -105,6 +107,10 @@ public class ReactiveAdminClient implements Closeable { .map(Tuple2::getT1) .collect(Collectors.toSet()); } + + static Set defaultFeatures() { + return Set.of(); + } } @Value @@ -127,9 +133,10 @@ public class ReactiveAdminClient implements Closeable { private static Mono> getSupportedUpdateFeaturesForVersion(AdminClient ac, String versionStr) { Float kafkaVersion = null; try { - kafkaVersion = NumberUtil.parserClusterVersion(versionStr); + float version = KafkaVersion.parse(versionStr); + return SupportedFeature.forVersion(version); } catch (NumberFormatException e) { - //Nothing to do here + return SupportedFeature.defaultFeatures(); } return SupportedFeature.forVersion(ac, kafkaVersion); } @@ -146,7 +153,7 @@ public class ReactiveAdminClient implements Closeable { // NOTE: if KafkaFuture returns null, that Mono will be empty(!), since Reactor does not support nullable results // (see MonoSink.success(..) javadoc for details) - private static Mono toMono(KafkaFuture future) { + public static Mono toMono(KafkaFuture future) { return Mono.create(sink -> future.whenComplete((res, ex) -> { if (ex != null) { // KafkaFuture doc is unclear about what exception wrapper will be used @@ -196,6 +203,7 @@ public class ReactiveAdminClient implements Closeable { } //NOTE: skips not-found topics (for which UnknownTopicOrPartitionException was thrown by AdminClient) + //and topics for which DESCRIBE_CONFIGS permission is not set (TopicAuthorizationException was thrown) public Mono>> getTopicsConfig(Collection topicNames, boolean includeDoc) { var includeDocFixed = features.contains(SupportedFeature.CONFIG_DOCUMENTATION_RETRIEVAL) && includeDoc; // we need to partition calls, because it can lead to AdminClient timeouts in case of large topics count @@ -216,7 +224,8 @@ public class ReactiveAdminClient implements Closeable { client.describeConfigs( resources, new DescribeConfigsOptions().includeSynonyms(true).includeDocumentation(includeDoc)).values(), - UnknownTopicOrPartitionException.class + UnknownTopicOrPartitionException.class, + TopicAuthorizationException.class ).map(config -> config.entrySet().stream() .collect(toMap( c -> c.getKey().name(), @@ -228,11 +237,17 @@ public class ReactiveAdminClient implements Closeable { .map(brokerId -> new ConfigResource(ConfigResource.Type.BROKER, Integer.toString(brokerId))) .collect(toList()); return toMono(client.describeConfigs(resources).all()) - .doOnError(InvalidRequestException.class, - th -> log.trace("Error while getting broker {} configs", brokerIds, th)) // some kafka backends (like MSK serverless) do not support broker's configs retrieval, // in that case InvalidRequestException will be thrown - .onErrorResume(InvalidRequestException.class, th -> Mono.just(Map.of())) + .onErrorResume(InvalidRequestException.class, th -> { + log.trace("Error while getting broker {} configs", brokerIds, th); + return Mono.just(Map.of()); + }) + // there are situations when kafka-ui user has no DESCRIBE_CONFIGS permission on cluster + .onErrorResume(ClusterAuthorizationException.class, th -> { + log.trace("AuthorizationException while getting configs for brokers {}", brokerIds, th); + return Mono.just(Map.of()); + }) .map(config -> config.entrySet().stream() .collect(toMap( c -> Integer.valueOf(c.getKey().name()), @@ -262,13 +277,16 @@ public class ReactiveAdminClient implements Closeable { private Mono> describeTopicsImpl(Collection topics) { return toMonoWithExceptionFilter( - client.describeTopics(topics).values(), - UnknownTopicOrPartitionException.class + client.describeTopics(topics).topicNameValues(), + UnknownTopicOrPartitionException.class, + // we only describe topics that we see from listTopics() API, so we should have permission to do it, + // but also adding this exception here for rare case when access restricted after we called listTopics() + TopicAuthorizationException.class ); } /** - * Returns TopicDescription mono, or Empty Mono if topic not found. + * Returns TopicDescription mono, or Empty Mono if topic not visible. */ public Mono describeTopic(String topic) { return describeTopics(List.of(topic)).flatMap(m -> Mono.justOrEmpty(m.get(topic))); @@ -282,10 +300,11 @@ public class ReactiveAdminClient implements Closeable { * such topics in resulting map. *

* This method converts input map into Mono[Map] ignoring keys for which KafkaFutures - * finished with clazz exception and empty Monos. + * finished with classes exceptions and empty Monos. */ + @SafeVarargs static Mono> toMonoWithExceptionFilter(Map> values, - Class clazz) { + Class... classes) { if (values.isEmpty()) { return Mono.just(Map.of()); } @@ -297,7 +316,7 @@ public class ReactiveAdminClient implements Closeable { .defaultIfEmpty(Tuples.of(e.getKey(), Optional.empty())) //tracking empty Monos .onErrorResume( // tracking Monos with suppressible error - th -> th.getClass().isAssignableFrom(clazz), + th -> Stream.of(classes).anyMatch(clazz -> th.getClass().isAssignableFrom(clazz)), th -> Mono.just(Tuples.of(e.getKey(), Optional.empty())))) .toList(); @@ -318,7 +337,13 @@ public class ReactiveAdminClient implements Closeable { public Mono>> describeLogDirs( Collection brokerIds) { - return toMono(client.describeLogDirs(brokerIds).all()); + return toMono(client.describeLogDirs(brokerIds).all()) + .onErrorResume(UnsupportedVersionException.class, th -> Mono.just(Map.of())) + .onErrorResume(ClusterAuthorizationException.class, th -> Mono.just(Map.of())) + .onErrorResume(th -> true, th -> { + log.warn("Error while calling describeLogDirs", th); + return Mono.just(Map.of()); + }); } public Mono describeCluster() { diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/StatisticsService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/StatisticsService.java index 9f0b938999..a36a64ff6d 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/StatisticsService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/StatisticsService.java @@ -1,6 +1,8 @@ package com.provectus.kafka.ui.service; -import com.provectus.kafka.ui.model.Feature; +import static com.provectus.kafka.ui.service.ReactiveAdminClient.ClusterDescription; + +import com.provectus.kafka.ui.model.ClusterFeature; import com.provectus.kafka.ui.model.InternalLogDirStats; import com.provectus.kafka.ui.model.KafkaCluster; import com.provectus.kafka.ui.model.Metrics; @@ -9,10 +11,12 @@ import com.provectus.kafka.ui.model.Statistics; import com.provectus.kafka.ui.service.metrics.MetricsCollector; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.admin.ConfigEntry; import org.apache.kafka.clients.admin.TopicDescription; +import org.apache.kafka.common.Node; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; @@ -21,7 +25,7 @@ import reactor.core.publisher.Mono; @Slf4j public class StatisticsService { - private final MetricsCollector metricsClusterUtil; + private final MetricsCollector metricsCollector; private final AdminClientService adminClientService; private final FeatureService featureService; private final StatisticsCache cache; @@ -35,8 +39,8 @@ public class StatisticsService { ac.describeCluster().flatMap(description -> Mono.zip( List.of( - metricsClusterUtil.getBrokerMetrics(cluster, description.getNodes()), - getLogDirInfo(cluster, ac), + metricsCollector.getBrokerMetrics(cluster, description.getNodes()), + getLogDirInfo(description, ac), featureService.getAvailableFeatures(cluster, description.getController()), loadTopicConfigs(cluster), describeTopics(cluster)), @@ -47,7 +51,7 @@ public class StatisticsService { .version(ac.getVersion()) .metrics((Metrics) results[0]) .logDirInfo((InternalLogDirStats) results[1]) - .features((List) results[2]) + .features((List) results[2]) .topicConfigs((Map>) results[3]) .topicDescriptions((Map) results[4]) .build() @@ -58,11 +62,9 @@ public class StatisticsService { e -> Mono.just(Statistics.empty().toBuilder().lastKafkaException(e).build())); } - private Mono getLogDirInfo(KafkaCluster cluster, ReactiveAdminClient c) { - if (!cluster.isDisableLogDirsCollection()) { - return c.describeLogDirs().map(InternalLogDirStats::new); - } - return Mono.just(InternalLogDirStats.empty()); + private Mono getLogDirInfo(ClusterDescription desc, ReactiveAdminClient ac) { + var brokerIds = desc.getNodes().stream().map(Node::id).collect(Collectors.toSet()); + return ac.describeLogDirs(brokerIds).map(InternalLogDirStats::new); } private Mono> describeTopics(KafkaCluster c) { diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/TopicsService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/TopicsService.java index ab22cecaaf..b172e2b4c6 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/TopicsService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/TopicsService.java @@ -7,7 +7,7 @@ import com.provectus.kafka.ui.exception.TopicMetadataException; import com.provectus.kafka.ui.exception.TopicNotFoundException; import com.provectus.kafka.ui.exception.TopicRecreationException; import com.provectus.kafka.ui.exception.ValidationException; -import com.provectus.kafka.ui.model.Feature; +import com.provectus.kafka.ui.model.ClusterFeature; import com.provectus.kafka.ui.model.InternalLogDirStats; import com.provectus.kafka.ui.model.InternalPartition; import com.provectus.kafka.ui.model.InternalPartitionsOffsets; @@ -162,9 +162,14 @@ public class TopicsService { } public Mono> getTopicConfigs(KafkaCluster cluster, String topicName) { + // there 2 case that we cover here: + // 1. topic not found/visible - describeTopic() will be empty and we will throw TopicNotFoundException + // 2. topic is visible, but we don't have DESCRIBE_CONFIG permission - we should return empty list return adminClientService.get(cluster) - .flatMap(ac -> ac.getTopicsConfig(List.of(topicName), true)) - .map(m -> m.values().stream().findFirst().orElseThrow(TopicNotFoundException::new)); + .flatMap(ac -> ac.describeTopic(topicName) + .switchIfEmpty(Mono.error(new TopicNotFoundException())) + .then(ac.getTopicsConfig(List.of(topicName), true)) + .map(m -> m.values().stream().findFirst().orElse(List.of()))); } private Mono createTopic(KafkaCluster c, ReactiveAdminClient adminClient, @@ -417,7 +422,7 @@ public class TopicsService { } public Mono deleteTopic(KafkaCluster cluster, String topicName) { - if (statisticsCache.get(cluster).getFeatures().contains(Feature.TOPIC_DELETION)) { + if (statisticsCache.get(cluster).getFeatures().contains(ClusterFeature.TOPIC_DELETION)) { return adminClientService.get(cluster).flatMap(c -> c.deleteTopic(topicName)) .doOnSuccess(t -> statisticsCache.onTopicDelete(cluster, topicName)); } else { diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/ConnectorInfo.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/ConnectorInfo.java new file mode 100644 index 0000000000..a7844579bf --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/ConnectorInfo.java @@ -0,0 +1,167 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import com.provectus.kafka.ui.model.ConnectorTypeDTO; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.apache.commons.collections.CollectionUtils; +import org.opendatadiscovery.oddrn.JdbcUrlParser; +import org.opendatadiscovery.oddrn.model.HivePath; +import org.opendatadiscovery.oddrn.model.MysqlPath; +import org.opendatadiscovery.oddrn.model.PostgreSqlPath; +import org.opendatadiscovery.oddrn.model.SnowflakePath; + +record ConnectorInfo(List inputs, + List outputs) { + + static ConnectorInfo extract(String className, + ConnectorTypeDTO type, + Map config, + List topicsFromApi, // can be empty for old Connect API versions + Function topicOddrnBuilder) { + return switch (className) { + case "org.apache.kafka.connect.file.FileStreamSinkConnector", + "org.apache.kafka.connect.file.FileStreamSourceConnector", + "FileStreamSource", + "FileStreamSink" -> extractFileIoConnector(type, topicsFromApi, config, topicOddrnBuilder); + case "io.confluent.connect.s3.S3SinkConnector" -> extractS3Sink(type, topicsFromApi, config, topicOddrnBuilder); + case "io.confluent.connect.jdbc.JdbcSinkConnector" -> + extractJdbcSink(type, topicsFromApi, config, topicOddrnBuilder); + case "io.debezium.connector.postgresql.PostgresConnector" -> extractDebeziumPg(config); + case "io.debezium.connector.mysql.MySqlConnector" -> extractDebeziumMysql(config); + default -> new ConnectorInfo( + extractInputs(type, topicsFromApi, config, topicOddrnBuilder), + extractOutputs(type, topicsFromApi, config, topicOddrnBuilder) + ); + }; + } + + private static ConnectorInfo extractFileIoConnector(ConnectorTypeDTO type, + List topics, + Map config, + Function topicOddrnBuilder) { + return new ConnectorInfo( + extractInputs(type, topics, config, topicOddrnBuilder), + extractOutputs(type, topics, config, topicOddrnBuilder) + ); + } + + private static ConnectorInfo extractJdbcSink(ConnectorTypeDTO type, + List topics, + Map config, + Function topicOddrnBuilder) { + String tableNameFormat = (String) config.getOrDefault("table.name.format", "${topic}"); + List targetTables = extractTopicNamesBestEffort(topics, config) + .map(topic -> tableNameFormat.replace("${kafka}", topic)) + .toList(); + + String connectionUrl = (String) config.get("connection.url"); + List outputs = new ArrayList<>(); + @Nullable var knownJdbcPath = new JdbcUrlParser().parse(connectionUrl); + if (knownJdbcPath instanceof PostgreSqlPath p) { + targetTables.forEach(t -> outputs.add(p.toBuilder().table(t).build().oddrn())); + } + if (knownJdbcPath instanceof MysqlPath p) { + targetTables.forEach(t -> outputs.add(p.toBuilder().table(t).build().oddrn())); + } + if (knownJdbcPath instanceof HivePath p) { + targetTables.forEach(t -> outputs.add(p.toBuilder().table(t).build().oddrn())); + } + if (knownJdbcPath instanceof SnowflakePath p) { + targetTables.forEach(t -> outputs.add(p.toBuilder().table(t).build().oddrn())); + } + return new ConnectorInfo( + extractInputs(type, topics, config, topicOddrnBuilder), + outputs + ); + } + + private static ConnectorInfo extractDebeziumPg(Map config) { + String host = (String) config.get("database.hostname"); + String dbName = (String) config.get("database.dbname"); + var inputs = List.of( + PostgreSqlPath.builder() + .host(host) + .database(dbName) + .build().oddrn() + ); + return new ConnectorInfo(inputs, List.of()); + } + + private static ConnectorInfo extractDebeziumMysql(Map config) { + String host = (String) config.get("database.hostname"); + var inputs = List.of( + MysqlPath.builder() + .host(host) + .build() + .oddrn() + ); + return new ConnectorInfo(inputs, List.of()); + } + + private static ConnectorInfo extractS3Sink(ConnectorTypeDTO type, + List topics, + Map config, + Function topicOrrdnBuilder) { + String bucketName = (String) config.get("s3.bucket.name"); + String topicsDir = (String) config.getOrDefault("topics.dir", "topics"); + String directoryDelim = (String) config.getOrDefault("directory.delim", "/"); + List outputs = extractTopicNamesBestEffort(topics, config) + .map(topic -> Oddrn.awsS3Oddrn(bucketName, topicsDir + directoryDelim + topic)) + .toList(); + return new ConnectorInfo( + extractInputs(type, topics, config, topicOrrdnBuilder), + outputs + ); + } + + private static List extractInputs(ConnectorTypeDTO type, + List topicsFromApi, + Map config, + Function topicOrrdnBuilder) { + return type == ConnectorTypeDTO.SINK + ? extractTopicsOddrns(config, topicsFromApi, topicOrrdnBuilder) + : List.of(); + } + + private static List extractOutputs(ConnectorTypeDTO type, + List topicsFromApi, + Map config, + Function topicOrrdnBuilder) { + return type == ConnectorTypeDTO.SOURCE + ? extractTopicsOddrns(config, topicsFromApi, topicOrrdnBuilder) + : List.of(); + } + + private static Stream extractTopicNamesBestEffort( + // topic list can be empty for old Connect API versions + List topicsFromApi, + Map config + ) { + if (CollectionUtils.isNotEmpty(topicsFromApi)) { + return topicsFromApi.stream(); + } + + // trying to extract topic names from config + String topicsString = (String) config.get("topics"); + String topicString = (String) config.get("topic"); + return Stream.of(topicsString, topicString) + .filter(Objects::nonNull) + .flatMap(str -> Stream.of(str.split(","))) + .map(String::trim) + .filter(s -> !s.isBlank()); + } + + private static List extractTopicsOddrns(Map config, + List topicsFromApi, + Function topicOrrdnBuilder) { + return extractTopicNamesBestEffort(topicsFromApi, config) + .map(topicOrrdnBuilder) + .toList(); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/ConnectorsExporter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/ConnectorsExporter.java new file mode 100644 index 0000000000..2fad00bbfa --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/ConnectorsExporter.java @@ -0,0 +1,96 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import com.provectus.kafka.ui.connect.model.ConnectorTopics; +import com.provectus.kafka.ui.model.ConnectDTO; +import com.provectus.kafka.ui.model.ConnectorDTO; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.service.KafkaConnectService; +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.opendatadiscovery.client.model.DataEntity; +import org.opendatadiscovery.client.model.DataEntityList; +import org.opendatadiscovery.client.model.DataEntityType; +import org.opendatadiscovery.client.model.DataSource; +import org.opendatadiscovery.client.model.DataTransformer; +import org.opendatadiscovery.client.model.MetadataExtension; +import reactor.core.publisher.Flux; + +@RequiredArgsConstructor +class ConnectorsExporter { + + private final KafkaConnectService kafkaConnectService; + + Flux export(KafkaCluster cluster) { + return kafkaConnectService.getConnects(cluster) + .flatMap(connect -> kafkaConnectService.getConnectorNames(cluster, connect.getName()) + .flatMap(connectorName -> kafkaConnectService.getConnector(cluster, connect.getName(), connectorName)) + .flatMap(connectorDTO -> + kafkaConnectService.getConnectorTopics(cluster, connect.getName(), connectorDTO.getName()) + .map(topics -> createConnectorDataEntity(cluster, connect, connectorDTO, topics))) + .buffer(100) + .map(connectDataEntities -> { + String dsOddrn = Oddrn.connectDataSourceOddrn(connect.getAddress()); + return new DataEntityList() + .dataSourceOddrn(dsOddrn) + .items(connectDataEntities); + }) + ); + } + + Flux getConnectDataSources(KafkaCluster cluster) { + return kafkaConnectService.getConnects(cluster) + .map(ConnectorsExporter::toDataSource); + } + + private static DataSource toDataSource(ConnectDTO connect) { + return new DataSource() + .oddrn(Oddrn.connectDataSourceOddrn(connect.getAddress())) + .name(connect.getName()) + .description("Kafka Connect"); + } + + private static DataEntity createConnectorDataEntity(KafkaCluster cluster, + ConnectDTO connect, + ConnectorDTO connector, + ConnectorTopics connectorTopics) { + var metadata = new HashMap<>(extractMetadata(connector)); + metadata.put("type", connector.getType().name()); + + var info = extractConnectorInfo(cluster, connector, connectorTopics); + DataTransformer transformer = new DataTransformer(); + transformer.setInputs(info.inputs()); + transformer.setOutputs(info.outputs()); + + return new DataEntity() + .oddrn(Oddrn.connectorOddrn(connect.getAddress(), connector.getName())) + .name(connector.getName()) + .description("Kafka Connector \"%s\" (%s)".formatted(connector.getName(), connector.getType())) + .type(DataEntityType.JOB) + .dataTransformer(transformer) + .metadata(List.of( + new MetadataExtension() + .schemaUrl(URI.create("wontbeused.oops")) + .metadata(metadata))); + } + + private static Map extractMetadata(ConnectorDTO connector) { + // will be sanitized by KafkaConfigSanitizer (if it's enabled) + return connector.getConfig(); + } + + private static ConnectorInfo extractConnectorInfo(KafkaCluster cluster, + ConnectorDTO connector, + ConnectorTopics topics) { + return ConnectorInfo.extract( + (String) connector.getConfig().get("connector.class"), + connector.getType(), + connector.getConfig(), + topics.getTopics(), + topic -> Oddrn.topicOddrn(cluster, topic) + ); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporter.java new file mode 100644 index 0000000000..2917e811be --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporter.java @@ -0,0 +1,106 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.service.KafkaConnectService; +import com.provectus.kafka.ui.service.StatisticsCache; +import java.util.List; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import lombok.SneakyThrows; +import org.opendatadiscovery.client.ApiClient; +import org.opendatadiscovery.client.api.OpenDataDiscoveryIngestionApi; +import org.opendatadiscovery.client.model.DataEntity; +import org.opendatadiscovery.client.model.DataEntityList; +import org.opendatadiscovery.client.model.DataSource; +import org.opendatadiscovery.client.model.DataSourceList; +import org.springframework.http.HttpHeaders; +import reactor.core.publisher.Mono; + +class OddExporter { + + private final OpenDataDiscoveryIngestionApi oddApi; + private final TopicsExporter topicsExporter; + private final ConnectorsExporter connectorsExporter; + + public OddExporter(StatisticsCache statisticsCache, + KafkaConnectService connectService, + OddIntegrationProperties oddIntegrationProperties) { + this( + createApiClient(oddIntegrationProperties), + new TopicsExporter(createTopicsFilter(oddIntegrationProperties), statisticsCache), + new ConnectorsExporter(connectService) + ); + } + + @VisibleForTesting + OddExporter(OpenDataDiscoveryIngestionApi oddApi, + TopicsExporter topicsExporter, + ConnectorsExporter connectorsExporter) { + this.oddApi = oddApi; + this.topicsExporter = topicsExporter; + this.connectorsExporter = connectorsExporter; + } + + private static Predicate createTopicsFilter(OddIntegrationProperties properties) { + if (properties.getTopicsRegex() == null) { + return topic -> !topic.startsWith("_"); + } + Pattern pattern = Pattern.compile(properties.getTopicsRegex()); + return topic -> pattern.matcher(topic).matches(); + } + + private static OpenDataDiscoveryIngestionApi createApiClient(OddIntegrationProperties properties) { + Preconditions.checkNotNull(properties.getUrl(), "ODD url not set"); + Preconditions.checkNotNull(properties.getToken(), "ODD token not set"); + var apiClient = new ApiClient() + .setBasePath(properties.getUrl()) + .addDefaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + properties.getToken()); + return new OpenDataDiscoveryIngestionApi(apiClient); + } + + public Mono export(KafkaCluster cluster) { + return exportTopics(cluster) + .then(exportKafkaConnects(cluster)); + } + + private Mono exportTopics(KafkaCluster c) { + return createKafkaDataSource(c) + .thenMany(topicsExporter.export(c)) + .concatMap(this::sentDataEntities) + .then(); + } + + private Mono exportKafkaConnects(KafkaCluster cluster) { + return createConnectDataSources(cluster) + .thenMany(connectorsExporter.export(cluster)) + .concatMap(this::sentDataEntities) + .then(); + } + + private Mono createConnectDataSources(KafkaCluster cluster) { + return connectorsExporter.getConnectDataSources(cluster) + .buffer(100) + .concatMap(dataSources -> oddApi.createDataSource(new DataSourceList().items(dataSources))) + .then(); + } + + private Mono createKafkaDataSource(KafkaCluster cluster) { + String clusterOddrn = Oddrn.clusterOddrn(cluster); + return oddApi.createDataSource( + new DataSourceList() + .addItemsItem( + new DataSource() + .oddrn(clusterOddrn) + .name(cluster.getName()) + .description("Kafka cluster") + ) + ); + } + + private Mono sentDataEntities(DataEntityList dataEntityList) { + return oddApi.postDataEntityList(dataEntityList); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporterScheduler.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporterScheduler.java new file mode 100644 index 0000000000..7201737f9f --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporterScheduler.java @@ -0,0 +1,27 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import com.provectus.kafka.ui.service.ClustersStorage; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Schedulers; + +@RequiredArgsConstructor +class OddExporterScheduler { + + private final ClustersStorage clustersStorage; + private final OddExporter oddExporter; + + @Scheduled(fixedRateString = "${kafka.send-stats-to-odd-millis:30000}") + public void sendMetricsToOdd() { + Flux.fromIterable(clustersStorage.getKafkaClusters()) + .parallel() + .runOn(Schedulers.parallel()) + .flatMap(oddExporter::export) + .then() + .block(); + } + + +} + diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddIntegrationConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddIntegrationConfig.java new file mode 100644 index 0000000000..6bade3022a --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddIntegrationConfig.java @@ -0,0 +1,31 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import com.provectus.kafka.ui.service.ClustersStorage; +import com.provectus.kafka.ui.service.KafkaConnectService; +import com.provectus.kafka.ui.service.StatisticsCache; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty(value = "integration.odd.url") +class OddIntegrationConfig { + + @Bean + OddIntegrationProperties oddIntegrationProperties() { + return new OddIntegrationProperties(); + } + + @Bean + OddExporter oddExporter(StatisticsCache statisticsCache, + KafkaConnectService connectService, + OddIntegrationProperties oddIntegrationProperties) { + return new OddExporter(statisticsCache, connectService, oddIntegrationProperties); + } + + @Bean + OddExporterScheduler oddExporterScheduler(ClustersStorage storage, OddExporter exporter) { + return new OddExporterScheduler(storage, exporter); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddIntegrationProperties.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddIntegrationProperties.java new file mode 100644 index 0000000000..cbb8d89238 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddIntegrationProperties.java @@ -0,0 +1,15 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + + +@Data +@ConfigurationProperties("integration.odd") +public class OddIntegrationProperties { + + String url; + String token; + String topicsRegex; + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/Oddrn.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/Oddrn.java new file mode 100644 index 0000000000..d228843b21 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/Oddrn.java @@ -0,0 +1,79 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import com.provectus.kafka.ui.model.KafkaCluster; +import java.net.URI; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.experimental.UtilityClass; +import org.opendatadiscovery.oddrn.Generator; +import org.opendatadiscovery.oddrn.model.AwsS3Path; +import org.opendatadiscovery.oddrn.model.KafkaConnectorPath; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +@UtilityClass +public class Oddrn { + + private static final Generator GENERATOR = new Generator(); + + String clusterOddrn(KafkaCluster cluster) { + return KafkaPath.builder() + .cluster(bootstrapServersForOddrn(cluster.getBootstrapServers())) + .build() + .oddrn(); + } + + KafkaPath topicOddrnPath(KafkaCluster cluster, String topic) { + return KafkaPath.builder() + .cluster(bootstrapServersForOddrn(cluster.getBootstrapServers())) + .topic(topic) + .build(); + } + + String topicOddrn(KafkaCluster cluster, String topic) { + return topicOddrnPath(cluster, topic).oddrn(); + } + + String awsS3Oddrn(String bucket, String key) { + return AwsS3Path.builder() + .bucket(bucket) + .key(key) + .build() + .oddrn(); + } + + String connectDataSourceOddrn(String connectUrl) { + return KafkaConnectorPath.builder() + .host(normalizedConnectHosts(connectUrl)) + .build() + .oddrn(); + } + + private String normalizedConnectHosts(String connectUrlStr) { + return Stream.of(connectUrlStr.split(",")) + .map(String::trim) + .sorted() + .map(url -> { + var uri = URI.create(url); + String host = uri.getHost(); + String portSuffix = (uri.getPort() > 0 ? (":" + uri.getPort()) : ""); + return host + portSuffix; + }) + .collect(Collectors.joining(",")); + } + + String connectorOddrn(String connectUrl, String connectorName) { + return KafkaConnectorPath.builder() + .host(normalizedConnectHosts(connectUrl)) + .connector(connectorName) + .build() + .oddrn(); + } + + private String bootstrapServersForOddrn(String bootstrapServers) { + return Stream.of(bootstrapServers.split(",")) + .map(String::trim) + .sorted() + .collect(Collectors.joining(",")); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporter.java new file mode 100644 index 0000000000..ad72e6f1dc --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporter.java @@ -0,0 +1,111 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import com.google.common.collect.ImmutableMap; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.Statistics; +import com.provectus.kafka.ui.service.StatisticsCache; +import com.provectus.kafka.ui.service.integration.odd.schema.DataSetFieldsExtractors; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.admin.ConfigEntry; +import org.apache.kafka.clients.admin.TopicDescription; +import org.opendatadiscovery.client.model.DataEntity; +import org.opendatadiscovery.client.model.DataEntityList; +import org.opendatadiscovery.client.model.DataEntityType; +import org.opendatadiscovery.client.model.DataSet; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.MetadataExtension; +import org.opendatadiscovery.oddrn.model.KafkaPath; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Slf4j +@RequiredArgsConstructor +class TopicsExporter { + + private final Predicate topicFilter; + private final StatisticsCache statisticsCache; + + Flux export(KafkaCluster cluster) { + String clusterOddrn = Oddrn.clusterOddrn(cluster); + Statistics stats = statisticsCache.get(cluster); + return Flux.fromIterable(stats.getTopicDescriptions().keySet()) + .filter(topicFilter) + .flatMap(topic -> createTopicDataEntity(cluster, topic, stats)) + .buffer(100) + .map(topicsEntities -> + new DataEntityList() + .dataSourceOddrn(clusterOddrn) + .items(topicsEntities)); + } + + private Mono createTopicDataEntity(KafkaCluster cluster, String topic, Statistics stats) { + KafkaPath topicOddrnPath = Oddrn.topicOddrnPath(cluster, topic); + return + Mono.zip( + getTopicSchema(cluster, topic, topicOddrnPath, true), + getTopicSchema(cluster, topic, topicOddrnPath, false) + ) + .map(keyValueFields -> { + var dataset = new DataSet(); + keyValueFields.getT1().forEach(dataset::addFieldListItem); + keyValueFields.getT2().forEach(dataset::addFieldListItem); + return new DataEntity() + .name(topic) + .description("Kafka topic \"%s\"".formatted(topic)) + .oddrn(Oddrn.topicOddrn(cluster, topic)) + .type(DataEntityType.KAFKA_TOPIC) + .dataset(dataset) + .addMetadataItem( + new MetadataExtension() + .schemaUrl(URI.create("wontbeused.oops")) + .metadata(getTopicMetadata(topic, stats))); + } + ); + } + + private Map getNonDefaultConfigs(String topic, Statistics stats) { + List config = stats.getTopicConfigs().get(topic); + if (config == null) { + return Map.of(); + } + return config.stream() + .filter(c -> c.source() == ConfigEntry.ConfigSource.DYNAMIC_TOPIC_CONFIG) + .collect(Collectors.toMap(ConfigEntry::name, ConfigEntry::value)); + } + + private Map getTopicMetadata(String topic, Statistics stats) { + TopicDescription topicDescription = stats.getTopicDescriptions().get(topic); + return ImmutableMap.builder() + .put("partitions", topicDescription.partitions().size()) + .put("replication_factor", topicDescription.partitions().get(0).replicas().size()) + .putAll(getNonDefaultConfigs(topic, stats)) + .build(); + } + + private Mono> getTopicSchema(KafkaCluster cluster, + String topic, + KafkaPath topicOddrn, + //currently we only retrieve value schema + boolean isKey) { + if (cluster.getSchemaRegistryClient() == null) { + return Mono.just(List.of()); + } + String subject = topic + (isKey ? "-key" : "-value"); + return cluster.getSchemaRegistryClient() + .mono(client -> client.getSubjectVersion(subject, "latest")) + .map(subj -> DataSetFieldsExtractors.extract(subj, topicOddrn, isKey)) + .onErrorResume(WebClientResponseException.NotFound.class, th -> Mono.just(List.of())) + .onErrorResume(th -> true, th -> { + log.warn("Error retrieving subject {} for cluster {}", subject, cluster.getName(), th); + return Mono.just(List.of()); + }); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/AvroExtractor.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/AvroExtractor.java new file mode 100644 index 0000000000..538bbde1a8 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/AvroExtractor.java @@ -0,0 +1,262 @@ +package com.provectus.kafka.ui.service.integration.odd.schema; + +import com.google.common.collect.ImmutableSet; +import com.provectus.kafka.ui.service.integration.odd.Oddrn; +import com.provectus.kafka.ui.sr.model.SchemaSubject; +import java.util.ArrayList; +import java.util.List; +import lombok.experimental.UtilityClass; +import org.apache.avro.Schema; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.DataSetFieldType; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +@UtilityClass +class AvroExtractor { + + static List extract(SchemaSubject subject, KafkaPath topicOddrn, boolean isKey) { + var schema = new Schema.Parser().parse(subject.getSchema()); + List result = new ArrayList<>(); + result.add(DataSetFieldsExtractors.rootField(topicOddrn, isKey)); + extract( + schema, + topicOddrn.oddrn() + "/columns/" + (isKey ? "key" : "value"), + null, + null, + null, + false, + ImmutableSet.of(), + result + ); + return result; + } + + private void extract(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + String doc, + Boolean nullable, + ImmutableSet registeredRecords, + List sink + ) { + switch (schema.getType()) { + case RECORD -> extractRecord(schema, parentOddr, oddrn, name, doc, nullable, registeredRecords, sink); + case UNION -> extractUnion(schema, parentOddr, oddrn, name, doc, registeredRecords, sink); + case ARRAY -> extractArray(schema, parentOddr, oddrn, name, doc, nullable, registeredRecords, sink); + case MAP -> extractMap(schema, parentOddr, oddrn, name, doc, nullable, registeredRecords, sink); + default -> extractPrimitive(schema, parentOddr, oddrn, name, doc, nullable, sink); + } + } + + private DataSetField createDataSetField(String name, + String doc, + String parentOddrn, + String oddrn, + Schema schema, + Boolean nullable) { + return new DataSetField() + .name(name) + .description(doc) + .parentFieldOddrn(parentOddrn) + .oddrn(oddrn) + .type(mapSchema(schema, nullable)); + } + + private void extractRecord(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + String doc, + Boolean nullable, + ImmutableSet registeredRecords, + List sink) { + boolean isRoot = oddrn == null; + if (!isRoot) { + sink.add(createDataSetField(name, doc, parentOddr, oddrn, schema, nullable)); + if (registeredRecords.contains(schema.getFullName())) { + // avoiding recursion by checking if record already registered in parsing chain + return; + } + } + var newRegisteredRecords = ImmutableSet.builder() + .addAll(registeredRecords) + .add(schema.getFullName()) + .build(); + + schema.getFields().forEach(f -> + extract( + f.schema(), + isRoot ? parentOddr : oddrn, + isRoot + ? parentOddr + "/" + f.name() + : oddrn + "/fields/" + f.name(), + f.name(), + f.doc(), + false, + newRegisteredRecords, + sink + )); + } + + private void extractUnion(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + String doc, + ImmutableSet registeredRecords, + List sink) { + boolean isRoot = oddrn == null; + boolean containsNull = schema.getTypes().stream().map(Schema::getType).anyMatch(t -> t == Schema.Type.NULL); + // if it is not root and there is only 2 values for union (null and smth else) + // we registering this field as optional without mentioning union + if (!isRoot && containsNull && schema.getTypes().size() == 2) { + var nonNullSchema = schema.getTypes().stream() + .filter(s -> s.getType() != Schema.Type.NULL) + .findFirst() + .orElseThrow(IllegalStateException::new); + extract( + nonNullSchema, + parentOddr, + oddrn, + name, + doc, + true, + registeredRecords, + sink + ); + return; + } + oddrn = isRoot ? parentOddr + "/union" : oddrn; + if (isRoot) { + sink.add(createDataSetField("Avro root union", doc, parentOddr, oddrn, schema, containsNull)); + } else { + sink.add(createDataSetField(name, doc, parentOddr, oddrn, schema, containsNull)); + } + for (Schema t : schema.getTypes()) { + if (t.getType() != Schema.Type.NULL) { + extract( + t, + oddrn, + oddrn + "/values/" + t.getName(), + t.getName(), + t.getDoc(), + containsNull, + registeredRecords, + sink + ); + } + } + } + + private void extractArray(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + String doc, + Boolean nullable, + ImmutableSet registeredRecords, + List sink) { + boolean isRoot = oddrn == null; + oddrn = isRoot ? parentOddr + "/array" : oddrn; + if (isRoot) { + sink.add(createDataSetField("Avro root Array", doc, parentOddr, oddrn, schema, nullable)); + } else { + sink.add(createDataSetField(name, doc, parentOddr, oddrn, schema, nullable)); + } + extract( + schema.getElementType(), + oddrn, + oddrn + "/items/" + schema.getElementType().getName(), + schema.getElementType().getName(), + schema.getElementType().getDoc(), + false, + registeredRecords, + sink + ); + } + + private void extractMap(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + String doc, + Boolean nullable, + ImmutableSet registeredRecords, + List sink) { + boolean isRoot = oddrn == null; + oddrn = isRoot ? parentOddr + "/map" : oddrn; + if (isRoot) { + sink.add(createDataSetField("Avro root map", doc, parentOddr, oddrn, schema, nullable)); + } else { + sink.add(createDataSetField(name, doc, parentOddr, oddrn, schema, nullable)); + } + extract( + new Schema.Parser().parse("\"string\""), + oddrn, + oddrn + "/key", + "key", + null, + nullable, + registeredRecords, + sink + ); + extract( + schema.getValueType(), + oddrn, + oddrn + "/value", + "value", + null, + nullable, + registeredRecords, + sink + ); + } + + + private void extractPrimitive(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + String doc, + Boolean nullable, + List sink) { + boolean isRoot = oddrn == null; + String primOddrn = isRoot ? (parentOddr + "/" + schema.getType()) : oddrn; + if (isRoot) { + sink.add(createDataSetField("Root avro " + schema.getType(), + doc, parentOddr, primOddrn, schema, nullable)); + } else { + sink.add(createDataSetField(name, doc, parentOddr, primOddrn, schema, nullable)); + } + } + + private DataSetFieldType.TypeEnum mapType(Schema.Type type) { + return switch (type) { + case INT, LONG -> DataSetFieldType.TypeEnum.INTEGER; + case FLOAT, DOUBLE, FIXED -> DataSetFieldType.TypeEnum.NUMBER; + case STRING, ENUM -> DataSetFieldType.TypeEnum.STRING; + case BOOLEAN -> DataSetFieldType.TypeEnum.BOOLEAN; + case BYTES -> DataSetFieldType.TypeEnum.BINARY; + case ARRAY -> DataSetFieldType.TypeEnum.LIST; + case RECORD -> DataSetFieldType.TypeEnum.STRUCT; + case MAP -> DataSetFieldType.TypeEnum.MAP; + case UNION -> DataSetFieldType.TypeEnum.UNION; + case NULL -> DataSetFieldType.TypeEnum.UNKNOWN; + }; + } + + private DataSetFieldType mapSchema(Schema schema, Boolean nullable) { + return new DataSetFieldType() + .logicalType(logicalType(schema)) + .isNullable(nullable) + .type(mapType(schema.getType())); + } + + private String logicalType(Schema schema) { + return schema.getType() == Schema.Type.RECORD + ? schema.getFullName() + : schema.getType().toString().toLowerCase(); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/DataSetFieldsExtractors.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/DataSetFieldsExtractors.java new file mode 100644 index 0000000000..746f172b57 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/DataSetFieldsExtractors.java @@ -0,0 +1,38 @@ +package com.provectus.kafka.ui.service.integration.odd.schema; + +import com.provectus.kafka.ui.service.integration.odd.Oddrn; +import com.provectus.kafka.ui.sr.model.SchemaSubject; +import com.provectus.kafka.ui.sr.model.SchemaType; +import java.util.List; +import java.util.Optional; +import lombok.experimental.UtilityClass; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.DataSetFieldType; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +@UtilityClass +public class DataSetFieldsExtractors { + + public List extract(SchemaSubject subject, KafkaPath topicOddrn, boolean isKey) { + SchemaType schemaType = Optional.ofNullable(subject.getSchemaType()).orElse(SchemaType.AVRO); + return switch (schemaType) { + case AVRO -> AvroExtractor.extract(subject, topicOddrn, isKey); + case JSON -> JsonSchemaExtractor.extract(subject, topicOddrn, isKey); + case PROTOBUF -> ProtoExtractor.extract(subject, topicOddrn, isKey); + }; + } + + + DataSetField rootField(KafkaPath topicOddrn, boolean isKey) { + var rootOddrn = topicOddrn.oddrn() + "/columns/" + (isKey ? "key" : "value"); + return new DataSetField() + .name(isKey ? "key" : "value") + .description("Topic's " + (isKey ? "key" : "value") + " schema") + .parentFieldOddrn(topicOddrn.oddrn()) + .oddrn(rootOddrn) + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .isNullable(true)); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/JsonSchemaExtractor.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/JsonSchemaExtractor.java new file mode 100644 index 0000000000..f92e1fc876 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/JsonSchemaExtractor.java @@ -0,0 +1,311 @@ +package com.provectus.kafka.ui.service.integration.odd.schema; + +import com.google.common.collect.ImmutableSet; +import com.provectus.kafka.ui.service.integration.odd.Oddrn; +import com.provectus.kafka.ui.sr.model.SchemaSubject; +import io.confluent.kafka.schemaregistry.json.JsonSchema; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.annotation.Nullable; +import lombok.experimental.UtilityClass; +import org.everit.json.schema.ArraySchema; +import org.everit.json.schema.BooleanSchema; +import org.everit.json.schema.CombinedSchema; +import org.everit.json.schema.FalseSchema; +import org.everit.json.schema.NullSchema; +import org.everit.json.schema.NumberSchema; +import org.everit.json.schema.ObjectSchema; +import org.everit.json.schema.ReferenceSchema; +import org.everit.json.schema.Schema; +import org.everit.json.schema.StringSchema; +import org.everit.json.schema.TrueSchema; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.DataSetFieldType; +import org.opendatadiscovery.client.model.MetadataExtension; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +@UtilityClass +class JsonSchemaExtractor { + + static List extract(SchemaSubject subject, KafkaPath topicOddrn, boolean isKey) { + Schema schema = new JsonSchema(subject.getSchema()).rawSchema(); + List result = new ArrayList<>(); + result.add(DataSetFieldsExtractors.rootField(topicOddrn, isKey)); + extract( + schema, + topicOddrn.oddrn() + "/columns/" + (isKey ? "key" : "value"), + null, + null, + null, + ImmutableSet.of(), + result + ); + return result; + } + + private void extract(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + Boolean nullable, + ImmutableSet registeredRecords, + List sink) { + if (schema instanceof ReferenceSchema s) { + Optional.ofNullable(s.getReferredSchema()) + .ifPresent(refSchema -> extract(refSchema, parentOddr, oddrn, name, nullable, registeredRecords, sink)); + } else if (schema instanceof ObjectSchema s) { + extractObject(s, parentOddr, oddrn, name, nullable, registeredRecords, sink); + } else if (schema instanceof ArraySchema s) { + extractArray(s, parentOddr, oddrn, name, nullable, registeredRecords, sink); + } else if (schema instanceof CombinedSchema cs) { + extractCombined(cs, parentOddr, oddrn, name, nullable, registeredRecords, sink); + } else if (schema instanceof BooleanSchema + || schema instanceof NumberSchema + || schema instanceof StringSchema + || schema instanceof NullSchema + ) { + extractPrimitive(schema, parentOddr, oddrn, name, nullable, sink); + } else { + extractUnknown(schema, parentOddr, oddrn, name, nullable, sink); + } + } + + private void extractPrimitive(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + Boolean nullable, + List sink) { + boolean isRoot = oddrn == null; + sink.add( + createDataSetField( + schema, + isRoot ? "Root JSON primitive" : name, + parentOddr, + isRoot ? (parentOddr + "/" + logicalTypeName(schema)) : oddrn, + mapType(schema), + logicalTypeName(schema), + nullable + ) + ); + } + + private void extractUnknown(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + Boolean nullable, + List sink) { + boolean isRoot = oddrn == null; + sink.add( + createDataSetField( + schema, + isRoot ? "Root type " + logicalTypeName(schema) : name, + parentOddr, + isRoot ? (parentOddr + "/" + logicalTypeName(schema)) : oddrn, + DataSetFieldType.TypeEnum.UNKNOWN, + logicalTypeName(schema), + nullable + ) + ); + } + + private void extractObject(ObjectSchema schema, + String parentOddr, + String oddrn, //null for root + String name, + Boolean nullable, + ImmutableSet registeredRecords, + List sink) { + boolean isRoot = oddrn == null; + // schemaLocation can be null for empty object schemas (like if it used in anyOf) + @Nullable var schemaLocation = schema.getSchemaLocation(); + if (!isRoot) { + sink.add(createDataSetField( + schema, + name, + parentOddr, + oddrn, + DataSetFieldType.TypeEnum.STRUCT, + logicalTypeName(schema), + nullable + )); + if (schemaLocation != null && registeredRecords.contains(schemaLocation)) { + // avoiding recursion by checking if record already registered in parsing chain + return; + } + } + + var newRegisteredRecords = schemaLocation == null + ? registeredRecords + : ImmutableSet.builder() + .addAll(registeredRecords) + .add(schemaLocation) + .build(); + + schema.getPropertySchemas().forEach((propertyName, propertySchema) -> { + boolean required = schema.getRequiredProperties().contains(propertyName); + extract( + propertySchema, + isRoot ? parentOddr : oddrn, + isRoot + ? parentOddr + "/" + propertyName + : oddrn + "/fields/" + propertyName, + propertyName, + !required, + newRegisteredRecords, + sink + ); + }); + } + + private void extractArray(ArraySchema schema, + String parentOddr, + String oddrn, //null for root + String name, + Boolean nullable, + ImmutableSet registeredRecords, + List sink) { + boolean isRoot = oddrn == null; + oddrn = isRoot ? parentOddr + "/array" : oddrn; + if (isRoot) { + sink.add( + createDataSetField( + schema, + "Json array root", + parentOddr, + oddrn, + DataSetFieldType.TypeEnum.LIST, + "array", + nullable + )); + } else { + sink.add( + createDataSetField( + schema, + name, + parentOddr, + oddrn, + DataSetFieldType.TypeEnum.LIST, + "array", + nullable + )); + } + @Nullable var itemsSchema = schema.getAllItemSchema(); + if (itemsSchema != null) { + extract( + itemsSchema, + oddrn, + oddrn + "/items/" + logicalTypeName(itemsSchema), + logicalTypeName(itemsSchema), + false, + registeredRecords, + sink + ); + } + } + + private void extractCombined(CombinedSchema schema, + String parentOddr, + String oddrn, //null for root + String name, + Boolean nullable, + ImmutableSet registeredRecords, + List sink) { + String combineType = "unknown"; + if (schema.getCriterion() == CombinedSchema.ALL_CRITERION) { + combineType = "allOf"; + } + if (schema.getCriterion() == CombinedSchema.ANY_CRITERION) { + combineType = "anyOf"; + } + if (schema.getCriterion() == CombinedSchema.ONE_CRITERION) { + combineType = "oneOf"; + } + + boolean isRoot = oddrn == null; + oddrn = isRoot ? (parentOddr + "/" + combineType) : (oddrn + "/" + combineType); + sink.add( + createDataSetField( + schema, + isRoot ? "Root %s".formatted(combineType) : name, + parentOddr, + oddrn, + DataSetFieldType.TypeEnum.UNION, + combineType, + nullable + ).addMetadataItem(new MetadataExtension() + .schemaUrl(URI.create("wontbeused.oops")) + .metadata(Map.of("criterion", combineType))) + ); + + for (Schema subschema : schema.getSubschemas()) { + extract( + subschema, + oddrn, + oddrn + "/values/" + logicalTypeName(subschema), + logicalTypeName(subschema), + nullable, + registeredRecords, + sink + ); + } + } + + private String getDescription(Schema schema) { + return Optional.ofNullable(schema.getTitle()) + .orElse(schema.getDescription()); + } + + private String logicalTypeName(Schema schema) { + return schema.getClass() + .getSimpleName() + .replace("Schema", ""); + } + + private DataSetField createDataSetField(Schema schema, + String name, + String parentOddrn, + String oddrn, + DataSetFieldType.TypeEnum type, + String logicalType, + Boolean nullable) { + return new DataSetField() + .name(name) + .parentFieldOddrn(parentOddrn) + .oddrn(oddrn) + .description(getDescription(schema)) + .type( + new DataSetFieldType() + .isNullable(nullable) + .logicalType(logicalType) + .type(type) + ); + } + + private DataSetFieldType.TypeEnum mapType(Schema type) { + if (type instanceof NumberSchema) { + return DataSetFieldType.TypeEnum.NUMBER; + } + if (type instanceof StringSchema) { + return DataSetFieldType.TypeEnum.STRING; + } + if (type instanceof BooleanSchema || type instanceof TrueSchema || type instanceof FalseSchema) { + return DataSetFieldType.TypeEnum.BOOLEAN; + } + if (type instanceof ObjectSchema) { + return DataSetFieldType.TypeEnum.STRUCT; + } + if (type instanceof ReferenceSchema s) { + return mapType(s.getReferredSchema()); + } + if (type instanceof CombinedSchema) { + return DataSetFieldType.TypeEnum.UNION; + } + return DataSetFieldType.TypeEnum.UNKNOWN; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/ProtoExtractor.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/ProtoExtractor.java new file mode 100644 index 0000000000..b4a5378239 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/ProtoExtractor.java @@ -0,0 +1,230 @@ +package com.provectus.kafka.ui.service.integration.odd.schema; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.BoolValue; +import com.google.protobuf.BytesValue; +import com.google.protobuf.Descriptors; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.DoubleValue; +import com.google.protobuf.Duration; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.StringValue; +import com.google.protobuf.Timestamp; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.protobuf.Value; +import com.provectus.kafka.ui.service.integration.odd.Oddrn; +import com.provectus.kafka.ui.sr.model.SchemaSubject; +import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import lombok.experimental.UtilityClass; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.DataSetFieldType; +import org.opendatadiscovery.client.model.DataSetFieldType.TypeEnum; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +@UtilityClass +class ProtoExtractor { + + private static final Set PRIMITIVES_WRAPPER_TYPE_NAMES = Set.of( + BoolValue.getDescriptor().getFullName(), + Int32Value.getDescriptor().getFullName(), + UInt32Value.getDescriptor().getFullName(), + Int64Value.getDescriptor().getFullName(), + UInt64Value.getDescriptor().getFullName(), + StringValue.getDescriptor().getFullName(), + BytesValue.getDescriptor().getFullName(), + FloatValue.getDescriptor().getFullName(), + DoubleValue.getDescriptor().getFullName() + ); + + List extract(SchemaSubject subject, KafkaPath topicOddrn, boolean isKey) { + Descriptor schema = new ProtobufSchema(subject.getSchema()).toDescriptor(); + List result = new ArrayList<>(); + result.add(DataSetFieldsExtractors.rootField(topicOddrn, isKey)); + var rootOddrn = topicOddrn.oddrn() + "/columns/" + (isKey ? "key" : "value"); + schema.getFields().forEach(f -> + extract(f, + rootOddrn, + rootOddrn + "/" + f.getName(), + f.getName(), + !f.isRequired(), + f.isRepeated(), + ImmutableSet.of(schema.getFullName()), + result + )); + return result; + } + + private void extract(Descriptors.FieldDescriptor field, + String parentOddr, + String oddrn, //null for root + String name, + boolean nullable, + boolean repeated, + ImmutableSet registeredRecords, + List sink) { + if (repeated) { + extractRepeated(field, parentOddr, oddrn, name, nullable, registeredRecords, sink); + } else if (field.getType() == Descriptors.FieldDescriptor.Type.MESSAGE) { + extractMessage(field, parentOddr, oddrn, name, nullable, registeredRecords, sink); + } else { + extractPrimitive(field, parentOddr, oddrn, name, nullable, sink); + } + } + + // converts some(!) Protobuf Well-known type (from google.protobuf.* packages) + // see JsonFormat::buildWellKnownTypePrinters for impl details + private boolean extractProtoWellKnownType(Descriptors.FieldDescriptor field, + String parentOddr, + String oddrn, //null for root + String name, + boolean nullable, + List sink) { + // all well-known types are messages + if (field.getType() != Descriptors.FieldDescriptor.Type.MESSAGE) { + return false; + } + String typeName = field.getMessageType().getFullName(); + if (typeName.equals(Timestamp.getDescriptor().getFullName())) { + sink.add(createDataSetField(name, parentOddr, oddrn, TypeEnum.DATETIME, typeName, nullable)); + return true; + } + if (typeName.equals(Duration.getDescriptor().getFullName())) { + sink.add(createDataSetField(name, parentOddr, oddrn, TypeEnum.DURATION, typeName, nullable)); + return true; + } + if (typeName.equals(Value.getDescriptor().getFullName())) { + //TODO: use ANY type when it will appear in ODD + sink.add(createDataSetField(name, parentOddr, oddrn, TypeEnum.UNKNOWN, typeName, nullable)); + return true; + } + if (PRIMITIVES_WRAPPER_TYPE_NAMES.contains(typeName)) { + var wrapped = field.getMessageType().findFieldByName("value"); + sink.add(createDataSetField(name, parentOddr, oddrn, mapType(wrapped.getType()), typeName, true)); + return true; + } + return false; + } + + private void extractRepeated(Descriptors.FieldDescriptor field, + String parentOddr, + String oddrn, //null for root + String name, + boolean nullable, + ImmutableSet registeredRecords, + List sink) { + sink.add(createDataSetField(name, parentOddr, oddrn, TypeEnum.LIST, "repeated", nullable)); + + String itemName = field.getType() == Descriptors.FieldDescriptor.Type.MESSAGE + ? field.getMessageType().getName() + : field.getType().name().toLowerCase(); + + extract( + field, + oddrn, + oddrn + "/items/" + itemName, + itemName, + nullable, + false, + registeredRecords, + sink + ); + } + + private void extractMessage(Descriptors.FieldDescriptor field, + String parentOddr, + String oddrn, //null for root + String name, + boolean nullable, + ImmutableSet registeredRecords, + List sink) { + if (extractProtoWellKnownType(field, parentOddr, oddrn, name, nullable, sink)) { + return; + } + sink.add(createDataSetField(name, parentOddr, oddrn, TypeEnum.STRUCT, getLogicalTypeName(field), nullable)); + + String msgTypeName = field.getMessageType().getFullName(); + if (registeredRecords.contains(msgTypeName)) { + // avoiding recursion by checking if record already registered in parsing chain + return; + } + var newRegisteredRecords = ImmutableSet.builder() + .addAll(registeredRecords) + .add(msgTypeName) + .build(); + + field.getMessageType() + .getFields() + .forEach(f -> { + extract(f, + oddrn, + oddrn + "/fields/" + f.getName(), + f.getName(), + !f.isRequired(), + f.isRepeated(), + newRegisteredRecords, + sink + ); + }); + } + + private void extractPrimitive(Descriptors.FieldDescriptor field, + String parentOddr, + String oddrn, + String name, + boolean nullable, + List sink) { + sink.add( + createDataSetField( + name, + parentOddr, + oddrn, + mapType(field.getType()), + getLogicalTypeName(field), + nullable + ) + ); + } + + private String getLogicalTypeName(Descriptors.FieldDescriptor f) { + return f.getType() == Descriptors.FieldDescriptor.Type.MESSAGE + ? f.getMessageType().getFullName() + : f.getType().name().toLowerCase(); + } + + private DataSetField createDataSetField(String name, + String parentOddrn, + String oddrn, + TypeEnum type, + String logicalType, + Boolean nullable) { + return new DataSetField() + .name(name) + .parentFieldOddrn(parentOddrn) + .oddrn(oddrn) + .type( + new DataSetFieldType() + .isNullable(nullable) + .logicalType(logicalType) + .type(type) + ); + } + + + private TypeEnum mapType(Descriptors.FieldDescriptor.Type type) { + return switch (type) { + case INT32, INT64, SINT32, SFIXED32, SINT64, UINT32, UINT64, FIXED32, FIXED64, SFIXED64 -> TypeEnum.INTEGER; + case FLOAT, DOUBLE -> TypeEnum.NUMBER; + case STRING, ENUM -> TypeEnum.STRING; + case BOOL -> TypeEnum.BOOLEAN; + case BYTES -> TypeEnum.BINARY; + case MESSAGE, GROUP -> TypeEnum.STRUCT; + }; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlApiClient.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlApiClient.java index e0b95c522a..fd68add726 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlApiClient.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlApiClient.java @@ -43,12 +43,13 @@ public class KsqlApiClient { UndefineVariableContext.class ); - @Builder + @Builder(toBuilder = true) @Value public static class KsqlResponseTable { String header; List columnNames; List> values; + boolean error; public Optional getColumnValue(List row, String column) { return Optional.ofNullable(row.get(columnNames.indexOf(column))); @@ -68,26 +69,22 @@ public class KsqlApiClient { public KsqlApiClient(String baseUrl, @Nullable ClustersProperties.KsqldbServerAuth ksqldbServerAuth, - @Nullable ClustersProperties.WebClientSsl ksqldbServerSsl, + @Nullable ClustersProperties.TruststoreConfig ksqldbServerSsl, + @Nullable ClustersProperties.KeystoreConfig keystoreConfig, @Nullable DataSize maxBuffSize) { this.baseUrl = baseUrl; - this.webClient = webClient(ksqldbServerAuth, ksqldbServerSsl, maxBuffSize); + this.webClient = webClient(ksqldbServerAuth, ksqldbServerSsl, keystoreConfig, maxBuffSize); } private static WebClient webClient(@Nullable ClustersProperties.KsqldbServerAuth ksqldbServerAuth, - @Nullable ClustersProperties.WebClientSsl ksqldbServerSsl, + @Nullable ClustersProperties.TruststoreConfig truststoreConfig, + @Nullable ClustersProperties.KeystoreConfig keystoreConfig, @Nullable DataSize maxBuffSize) { ksqldbServerAuth = Optional.ofNullable(ksqldbServerAuth).orElse(new ClustersProperties.KsqldbServerAuth()); - ksqldbServerSsl = Optional.ofNullable(ksqldbServerSsl).orElse(new ClustersProperties.WebClientSsl()); maxBuffSize = Optional.ofNullable(maxBuffSize).orElse(DataSize.ofMegabytes(20)); return new WebClientConfigurator() - .configureSsl( - ksqldbServerSsl.getKeystoreLocation(), - ksqldbServerSsl.getKeystorePassword(), - ksqldbServerSsl.getTruststoreLocation(), - ksqldbServerSsl.getTruststorePassword() - ) + .configureSsl(truststoreConfig, keystoreConfig) .configureBasicAuth( ksqldbServerAuth.getUsername(), ksqldbServerAuth.getPassword() diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/response/ResponseParser.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/response/ResponseParser.java index 4781d159e7..cd91fa57dc 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/response/ResponseParser.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/response/ResponseParser.java @@ -3,14 +3,13 @@ package com.provectus.kafka.ui.service.ksql.response; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.provectus.kafka.ui.exception.KsqlApiException; import com.provectus.kafka.ui.service.ksql.KsqlApiClient; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; import org.springframework.web.reactive.function.client.WebClientResponseException; public class ResponseParser { @@ -24,11 +23,7 @@ public class ResponseParser { return Optional.of( KsqlApiClient.KsqlResponseTable.builder() .header("Schema") - .columnNames( - Arrays.stream(jsonNode.get("header").get("schema").asText().split(",")) - .map(String::trim) - .collect(Collectors.toList()) - ) + .columnNames(parseSelectHeadersString(jsonNode.get("header").get("schema").asText())) .build()); } if (arrayFieldNonEmpty(jsonNode, "row")) { @@ -46,18 +41,50 @@ public class ResponseParser { return Optional.empty(); } + @VisibleForTesting + static List parseSelectHeadersString(String str) { + List headers = new ArrayList<>(); + int structNesting = 0; + boolean quotes = false; + var headerBuilder = new StringBuilder(); + for (char ch : str.toCharArray()) { + if (ch == '<') { + structNesting++; + } else if (ch == '>') { + structNesting--; + } else if (ch == '`') { + quotes = !quotes; + } else if (ch == ' ' && headerBuilder.isEmpty()) { + continue; //skipping leading & training whitespaces + } else if (ch == ',' && structNesting == 0 && !quotes) { + headers.add(headerBuilder.toString()); + headerBuilder = new StringBuilder(); + continue; + } + headerBuilder.append(ch); + } + if (!headerBuilder.isEmpty()) { + headers.add(headerBuilder.toString()); + } + return headers; + } + public static KsqlApiClient.KsqlResponseTable errorTableWithTextMsg(String errorText) { return KsqlApiClient.KsqlResponseTable.builder() .header("Execution error") .columnNames(List.of("message")) .values(List.of(List.of(new TextNode(errorText)))) + .error(true) .build(); } public static KsqlApiClient.KsqlResponseTable parseErrorResponse(WebClientResponseException e) { try { var errBody = new JsonMapper().readTree(e.getResponseBodyAsString()); - return DynamicParser.parseObject("Execution error", errBody); + return DynamicParser.parseObject("Execution error", errBody) + .toBuilder() + .error(true) + .build(); } catch (Exception ex) { return errorTableWithTextMsg( String.format( diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/DataMasking.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/DataMasking.java index a8a69ad752..78e74f3332 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/DataMasking.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/DataMasking.java @@ -41,9 +41,9 @@ public class DataMasking { private final List masks; - public static DataMasking create(List config) { + public static DataMasking create(@Nullable List config) { return new DataMasking( - config.stream().map(property -> { + Optional.ofNullable(config).orElse(List.of()).stream().map(property -> { Preconditions.checkNotNull(property.getType(), "masking type not specifed"); Preconditions.checkArgument( StringUtils.isNotEmpty(property.getTopicKeysPattern()) diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Mask.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Mask.java index 1459e95f00..dbbc5d131a 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Mask.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Mask.java @@ -11,6 +11,8 @@ import java.util.function.UnaryOperator; class Mask extends MaskingPolicy { + static final List DEFAULT_PATTERN = List.of("X", "x", "n", "-"); + private final UnaryOperator masker; Mask(List fieldNames, List maskingChars) { diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/MaskingPolicy.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/MaskingPolicy.java index 25a463a9da..7a75338210 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/MaskingPolicy.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/MaskingPolicy.java @@ -1,7 +1,6 @@ package com.provectus.kafka.ui.service.masking.policies; import com.fasterxml.jackson.databind.node.ContainerNode; -import com.google.common.base.Preconditions; import com.provectus.kafka.ui.config.ClustersProperties; import java.util.List; import lombok.RequiredArgsConstructor; @@ -9,15 +8,28 @@ import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public abstract class MaskingPolicy { + public static MaskingPolicy create(ClustersProperties.Masking property) { - Preconditions.checkNotNull(property.getFields()); + List fields = property.getFields() == null + ? List.of() // empty list means that policy will be applied to all fields + : property.getFields(); switch (property.getType()) { case REMOVE: - return new Remove(property.getFields()); + return new Remove(fields); case REPLACE: - return new Replace(property.getFields(), property.getReplacement()); + return new Replace( + fields, + property.getReplacement() == null + ? Replace.DEFAULT_REPLACEMENT + : property.getReplacement() + ); case MASK: - return new Mask(property.getFields(), property.getPattern()); + return new Mask( + fields, + property.getPattern() == null + ? Mask.DEFAULT_PATTERN + : property.getPattern() + ); default: throw new IllegalStateException("Unknown policy type: " + property.getType()); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Replace.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Replace.java index a335730258..3af645cb11 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Replace.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Replace.java @@ -10,6 +10,8 @@ import java.util.List; class Replace extends MaskingPolicy { + static final String DEFAULT_REPLACEMENT = "***DATA_MASKED***"; + private final String replacement; Replace(List fieldNames, String replacementString) { diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxMetricsRetriever.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxMetricsRetriever.java index 8a6e10656d..78f5bdeced 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxMetricsRetriever.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxMetricsRetriever.java @@ -1,21 +1,22 @@ package com.provectus.kafka.ui.service.metrics; -import com.provectus.kafka.ui.model.JmxConnectionInfo; import com.provectus.kafka.ui.model.KafkaCluster; -import com.provectus.kafka.ui.util.JmxPoolFactory; +import java.io.Closeable; import java.util.ArrayList; -import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.function.Consumer; import javax.management.MBeanAttributeInfo; import javax.management.MBeanServerConnection; import javax.management.ObjectName; import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.pool2.impl.GenericKeyedObjectPool; -import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig; +import org.apache.commons.lang3.StringUtils; import org.apache.kafka.common.Node; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -23,68 +24,102 @@ import reactor.core.scheduler.Schedulers; @Service -@Lazy @Slf4j -class JmxMetricsRetriever implements MetricsRetriever, AutoCloseable { +class JmxMetricsRetriever implements MetricsRetriever, Closeable { + + private static final boolean SSL_JMX_SUPPORTED; + + static { + // see JmxSslSocketFactory doc for details + SSL_JMX_SUPPORTED = JmxSslSocketFactory.initialized(); + } private static final String JMX_URL = "service:jmx:rmi:///jndi/rmi://"; private static final String JMX_SERVICE_TYPE = "jmxrmi"; private static final String CANONICAL_NAME_PATTERN = "kafka.server*:*"; - private final GenericKeyedObjectPool pool; - - public JmxMetricsRetriever() { - this.pool = new GenericKeyedObjectPool<>(new JmxPoolFactory()); - GenericKeyedObjectPoolConfig poolConfig = new GenericKeyedObjectPoolConfig<>(); - poolConfig.setMaxIdlePerKey(3); - poolConfig.setMaxTotalPerKey(3); - this.pool.setConfig(poolConfig); + @Override + public void close() { + JmxSslSocketFactory.clearFactoriesCache(); } @Override public Flux retrieve(KafkaCluster c, Node node) { + if (isSslJmxEndpoint(c) && !SSL_JMX_SUPPORTED) { + log.warn("Cluster {} has jmx ssl configured, but it is not supported", c.getName()); + return Flux.empty(); + } return Mono.fromSupplier(() -> retrieveSync(c, node)) .subscribeOn(Schedulers.boundedElastic()) .flatMapMany(Flux::fromIterable); } + private boolean isSslJmxEndpoint(KafkaCluster cluster) { + return cluster.getMetricsConfig().getKeystoreLocation() != null; + } + + @SneakyThrows private List retrieveSync(KafkaCluster c, Node node) { String jmxUrl = JMX_URL + node.host() + ":" + c.getMetricsConfig().getPort() + "/" + JMX_SERVICE_TYPE; log.debug("Collection JMX metrics for {}", jmxUrl); - final var connectionInfo = JmxConnectionInfo.builder() - .url(jmxUrl) - .ssl(c.getMetricsConfig().isSsl()) - .username(c.getMetricsConfig().getUsername()) - .password(c.getMetricsConfig().getPassword()) - .build(); - JMXConnector srv; - try { - srv = pool.borrowObject(connectionInfo); - } catch (Exception e) { - log.error("Cannot get JMX connector for the pool due to: ", e); - return Collections.emptyList(); - } List result = new ArrayList<>(); - try { - MBeanServerConnection msc = srv.getMBeanServerConnection(); - var jmxMetrics = msc.queryNames(new ObjectName(CANONICAL_NAME_PATTERN), null); - for (ObjectName jmxMetric : jmxMetrics) { - result.addAll(extractObjectMetrics(jmxMetric, msc)); - } - pool.returnObject(connectionInfo, srv); - } catch (Exception e) { - log.error("Error getting jmx metrics from {}", jmxUrl, e); - closeConnectionExceptionally(jmxUrl, srv); - } + withJmxConnector(jmxUrl, c, jmxConnector -> getMetricsFromJmx(jmxConnector, result)); log.debug("{} metrics collected for {}", result.size(), jmxUrl); return result; } - private void closeConnectionExceptionally(String url, JMXConnector srv) { + private void withJmxConnector(String jmxUrl, + KafkaCluster c, + Consumer consumer) { + var env = prepareJmxEnvAndSetThreadLocal(c); try { - pool.invalidateObject(new JmxConnectionInfo(url), srv); + JMXConnector connector = null; + try { + connector = JMXConnectorFactory.newJMXConnector(new JMXServiceURL(jmxUrl), env); + connector.connect(env); + } catch (Exception exception) { + log.error("Error connecting to {}", jmxUrl, exception); + return; + } + consumer.accept(connector); + connector.close(); } catch (Exception e) { - log.error("Cannot invalidate object in pool, {}", url, e); + log.error("Error getting jmx metrics from {}", jmxUrl, e); + } finally { + JmxSslSocketFactory.clearThreadLocalContext(); + } + } + + private Map prepareJmxEnvAndSetThreadLocal(KafkaCluster cluster) { + var metricsConfig = cluster.getMetricsConfig(); + Map env = new HashMap<>(); + if (isSslJmxEndpoint(cluster)) { + var clusterSsl = cluster.getOriginalProperties().getSsl(); + JmxSslSocketFactory.setSslContextThreadLocal( + clusterSsl != null ? clusterSsl.getTruststoreLocation() : null, + clusterSsl != null ? clusterSsl.getTruststorePassword() : null, + metricsConfig.getKeystoreLocation(), + metricsConfig.getKeystorePassword() + ); + JmxSslSocketFactory.editJmxConnectorEnv(env); + } + + if (StringUtils.isNotEmpty(metricsConfig.getUsername()) + && StringUtils.isNotEmpty(metricsConfig.getPassword())) { + env.put( + JMXConnector.CREDENTIALS, + new String[] {metricsConfig.getUsername(), metricsConfig.getPassword()} + ); + } + return env; + } + + @SneakyThrows + private void getMetricsFromJmx(JMXConnector jmxConnector, List sink) { + MBeanServerConnection msc = jmxConnector.getMBeanServerConnection(); + var jmxMetrics = msc.queryNames(new ObjectName(CANONICAL_NAME_PATTERN), null); + for (ObjectName jmxMetric : jmxMetrics) { + sink.addAll(extractObjectMetrics(jmxMetric, msc)); } } @@ -98,9 +133,5 @@ class JmxMetricsRetriever implements MetricsRetriever, AutoCloseable { return JmxMetricsFormatter.constructMetricsList(objectName, attrNames, attrValues); } - @Override - public void close() { - this.pool.close(); - } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxSslSocketFactory.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxSslSocketFactory.java new file mode 100644 index 0000000000..06304365c7 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxSslSocketFactory.java @@ -0,0 +1,218 @@ +package com.provectus.kafka.ui.service.metrics; + +import com.google.common.base.Preconditions; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyStore; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import javax.rmi.ssl.SslRMIClientSocketFactory; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.ResourceUtils; + +/* + * Purpose of this class to provide an ability to connect to different JMX endpoints using different keystores. + * + * Usually, when you want to establish SSL JMX connection you set "com.sun.jndi.rmi.factory.socket" env + * property to SslRMIClientSocketFactory instance. SslRMIClientSocketFactory itself uses SSLSocketFactory.getDefault() + * as a socket factory implementation. Problem here is that when ones SslRMIClientSocketFactory instance is created, + * the same cached SSLSocketFactory instance will be used to establish connection with *all* JMX endpoints. + * Moreover, even if we submit custom SslRMIClientSocketFactory implementation which takes specific ssl context + * into account, SslRMIClientSocketFactory is + * internally created during RMI calls. + * + * So, the only way we found to deal with it is to change internal field ('defaultSocketFactory') of + * SslRMIClientSocketFactory to our custom impl, and left all internal RMI code work as is. + * Since RMI code is synchronous, we can pass parameters (which are truststore/keystore) to our custom factory + * that we want to use when creating ssl socket via ThreadLocal variables. + * + * NOTE 1: Theoretically we could avoid using reflection to set internal field set by + * setting "ssl.SocketFactory.provider" security property (see code in SSLSocketFactory.getDefault()), + * but that code uses systemClassloader which is not working right when we're creating executable spring boot jar + * (https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html#appendix.executable-jar.restrictions). + * We can use this if we swith to other jar-packing solutions in the future. + * + * NOTE 2: There are two paths from which socket factory is called - when jmx connection if established (we manage this + * by passing ThreadLocal vars) and from DGCClient in background thread - we deal with that we cache created factories + * for specific host+port. + * + */ +@Slf4j +class JmxSslSocketFactory extends javax.net.ssl.SSLSocketFactory { + + private static final boolean SSL_JMX_SUPPORTED; + + static { + boolean sslJmxSupported = false; + try { + Field defaultSocketFactoryField = SslRMIClientSocketFactory.class.getDeclaredField("defaultSocketFactory"); + defaultSocketFactoryField.setAccessible(true); + defaultSocketFactoryField.set(null, new JmxSslSocketFactory()); + sslJmxSupported = true; + } catch (Exception e) { + log.error("----------------------------------"); + log.error("SSL can't be enabled for JMX retrieval. " + + "Make sure your java app run with '--add-opens java.rmi/javax.rmi.ssl=ALL-UNNAMED' arg.", e); + log.error("----------------------------------"); + } + SSL_JMX_SUPPORTED = sslJmxSupported; + } + + public static boolean initialized() { + return SSL_JMX_SUPPORTED; + } + + private static final ThreadLocal SSL_CONTEXT_THREAD_LOCAL = new ThreadLocal<>(); + + private static final Map CACHED_FACTORIES = new ConcurrentHashMap<>(); + + private record HostAndPort(String host, int port) { + } + + private record Ssl(@Nullable String truststoreLocation, + @Nullable String truststorePassword, + @Nullable String keystoreLocation, + @Nullable String keystorePassword) { + } + + public static void setSslContextThreadLocal(@Nullable String truststoreLocation, + @Nullable String truststorePassword, + @Nullable String keystoreLocation, + @Nullable String keystorePassword) { + SSL_CONTEXT_THREAD_LOCAL.set( + new Ssl(truststoreLocation, truststorePassword, keystoreLocation, keystorePassword)); + } + + // should be called when (host:port) -> factory cache should be invalidated (ex. on app config reload) + public static void clearFactoriesCache() { + CACHED_FACTORIES.clear(); + } + + public static void clearThreadLocalContext() { + SSL_CONTEXT_THREAD_LOCAL.set(null); + } + + public static void editJmxConnectorEnv(Map env) { + env.put("com.sun.jndi.rmi.factory.socket", new SslRMIClientSocketFactory()); + } + + //----------------------------------------------------------------------------------------------- + + private final javax.net.ssl.SSLSocketFactory defaultSocketFactory; + + @SneakyThrows + public JmxSslSocketFactory() { + this.defaultSocketFactory = SSLContext.getDefault().getSocketFactory(); + } + + @SneakyThrows + private javax.net.ssl.SSLSocketFactory createFactoryFromThreadLocalCtx() { + Ssl ssl = Preconditions.checkNotNull(SSL_CONTEXT_THREAD_LOCAL.get()); + + var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + if (ssl.truststoreLocation() != null && ssl.truststorePassword() != null) { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load( + new FileInputStream((ResourceUtils.getFile(ssl.truststoreLocation()))), + ssl.truststorePassword().toCharArray() + ); + trustManagerFactory.init(trustStore); + } + + var keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + if (ssl.keystoreLocation() != null && ssl.keystorePassword() != null) { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load( + new FileInputStream(ResourceUtils.getFile(ssl.keystoreLocation())), + ssl.keystorePassword().toCharArray() + ); + keyManagerFactory.init(keyStore, ssl.keystorePassword().toCharArray()); + } + + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init( + keyManagerFactory.getKeyManagers(), + trustManagerFactory.getTrustManagers(), + null + ); + return ctx.getSocketFactory(); + } + + private boolean threadLocalContextSet() { + return SSL_CONTEXT_THREAD_LOCAL.get() != null; + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + var hostAndPort = new HostAndPort(host, port); + if (CACHED_FACTORIES.containsKey(hostAndPort)) { + return CACHED_FACTORIES.get(hostAndPort).createSocket(host, port); + } else if (threadLocalContextSet()) { + var factory = createFactoryFromThreadLocalCtx(); + CACHED_FACTORIES.put(hostAndPort, factory); + return factory.createSocket(host, port); + } + return defaultSocketFactory.createSocket(host, port); + } + + /// FOLLOWING METHODS WON'T BE USED DURING JMX INTERACTION, IMPLEMENTING THEM JUST FOR CONSISTENCY ->>>>> + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + if (threadLocalContextSet()) { + return createFactoryFromThreadLocalCtx().createSocket(s, host, port, autoClose); + } + return defaultSocketFactory.createSocket(s, host, port, autoClose); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException, UnknownHostException { + if (threadLocalContextSet()) { + return createFactoryFromThreadLocalCtx().createSocket(host, port, localHost, localPort); + } + return defaultSocketFactory.createSocket(host, port, localHost, localPort); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + if (threadLocalContextSet()) { + return createFactoryFromThreadLocalCtx().createSocket(host, port); + } + return defaultSocketFactory.createSocket(host, port); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) + throws IOException { + if (threadLocalContextSet()) { + return createFactoryFromThreadLocalCtx().createSocket(address, port, localAddress, localPort); + } + return defaultSocketFactory.createSocket(address, port, localAddress, localPort); + } + + @Override + public String[] getDefaultCipherSuites() { + if (threadLocalContextSet()) { + return createFactoryFromThreadLocalCtx().getDefaultCipherSuites(); + } + return defaultSocketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + if (threadLocalContextSet()) { + return createFactoryFromThreadLocalCtx().getSupportedCipherSuites(); + } + return defaultSocketFactory.getSupportedCipherSuites(); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetriever.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetriever.java index e4cb4c36cb..33ef1b8072 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetriever.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetriever.java @@ -2,53 +2,58 @@ package com.provectus.kafka.ui.service.metrics; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import com.provectus.kafka.ui.config.ClustersProperties; import com.provectus.kafka.ui.model.KafkaCluster; import com.provectus.kafka.ui.model.MetricsConfig; +import com.provectus.kafka.ui.util.WebClientConfigurator; import java.util.Arrays; import java.util.Optional; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.common.Node; import org.springframework.stereotype.Service; +import org.springframework.util.unit.DataSize; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @Service -@RequiredArgsConstructor @Slf4j class PrometheusMetricsRetriever implements MetricsRetriever { private static final String METRICS_ENDPOINT_PATH = "/metrics"; private static final int DEFAULT_EXPORTER_PORT = 11001; - private final WebClient webClient; - @Override public Flux retrieve(KafkaCluster c, Node node) { log.debug("Retrieving metrics from prometheus exporter: {}:{}", node.host(), c.getMetricsConfig().getPort()); - return retrieve(node.host(), c.getMetricsConfig()); + + MetricsConfig metricsConfig = c.getMetricsConfig(); + var webClient = new WebClientConfigurator() + .configureBufferSize(DataSize.ofMegabytes(20)) + .configureBasicAuth(metricsConfig.getUsername(), metricsConfig.getPassword()) + .configureSsl( + c.getOriginalProperties().getSsl(), + new ClustersProperties.KeystoreConfig( + metricsConfig.getKeystoreLocation(), + metricsConfig.getKeystorePassword())) + .build(); + + return retrieve(webClient, node.host(), c.getMetricsConfig()); } @VisibleForTesting - Flux retrieve(String host, MetricsConfig metricsConfig) { + Flux retrieve(WebClient webClient, String host, MetricsConfig metricsConfig) { int port = Optional.ofNullable(metricsConfig.getPort()).orElse(DEFAULT_EXPORTER_PORT); - + boolean sslEnabled = metricsConfig.isSsl() || metricsConfig.getKeystoreLocation() != null; var request = webClient.get() .uri(UriComponentsBuilder.newInstance() - .scheme(metricsConfig.isSsl() ? "https" : "http") + .scheme(sslEnabled ? "https" : "http") .host(host) .port(port) .path(METRICS_ENDPOINT_PATH).build().toUri()); - if (metricsConfig.getUsername() != null && metricsConfig.getPassword() != null) { - request.headers( - httpHeaders -> httpHeaders.setBasicAuth(metricsConfig.getUsername(), metricsConfig.getPassword())); - } - WebClient.ResponseSpec responseSpec = request.retrieve(); - return responseSpec.bodyToMono(String.class) .doOnError(e -> log.error("Error while getting metrics from {}", host, e)) .onErrorResume(th -> Mono.empty()) diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java index 32cde63adb..ee17d21111 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java @@ -1,5 +1,7 @@ package com.provectus.kafka.ui.service.rbac; +import static com.provectus.kafka.ui.model.rbac.Resource.APPLICATIONCONFIG; + import com.provectus.kafka.ui.config.auth.AuthenticatedUser; import com.provectus.kafka.ui.config.auth.RbacUser; import com.provectus.kafka.ui.config.auth.RoleBasedAccessControlProperties; @@ -55,7 +57,7 @@ public class AccessControlService { @PostConstruct public void init() { - if (properties.getRoles().isEmpty()) { + if (CollectionUtils.isEmpty(properties.getRoles())) { log.trace("No roles provided, disabling RBAC"); return; } @@ -88,7 +90,8 @@ public class AccessControlService { return getUser() .doOnNext(user -> { boolean accessGranted = - isClusterAccessible(context, user) + isApplicationConfigAccessible(context, user) + && isClusterAccessible(context, user) && isClusterConfigAccessible(context, user) && isTopicAccessible(context, user) && isConsumerGroupAccessible(context, user) @@ -112,6 +115,20 @@ public class AccessControlService { .map(user -> new AuthenticatedUser(user.name(), user.groups())); } + public boolean isApplicationConfigAccessible(AccessContext context, AuthenticatedUser user) { + if (!rbacEnabled) { + return true; + } + if (CollectionUtils.isEmpty(context.getApplicationConfigActions())) { + return true; + } + Set requiredActions = context.getApplicationConfigActions() + .stream() + .map(a -> a.toString().toUpperCase()) + .collect(Collectors.toSet()); + return isAccessible(APPLICATIONCONFIG, null, user, context, requiredActions); + } + private boolean isClusterAccessible(AccessContext context, AuthenticatedUser user) { if (!rbacEnabled) { return true; @@ -348,12 +365,12 @@ public class AccessControlService { return Collections.unmodifiableList(properties.getRoles()); } - private boolean isAccessible(Resource resource, String resourceValue, + private boolean isAccessible(Resource resource, @Nullable String resourceValue, AuthenticatedUser user, AccessContext context, Set requiredActions) { Set grantedActions = properties.getRoles() .stream() .filter(filterRole(user)) - .filter(filterCluster(context.getCluster())) + .filter(filterCluster(resource, context.getCluster())) .flatMap(grantedRole -> grantedRole.getPermissions().stream()) .filter(filterResource(resource)) .filter(filterResourceValue(resourceValue)) @@ -374,21 +391,28 @@ public class AccessControlService { .anyMatch(cluster::equalsIgnoreCase); } + private Predicate filterCluster(Resource resource, String cluster) { + if (resource == APPLICATIONCONFIG) { + return role -> true; + } + return filterCluster(cluster); + } + private Predicate filterResource(Resource resource) { return grantedPermission -> resource == grantedPermission.getResource(); } - private Predicate filterResourceValue(String resourceValue) { + private Predicate filterResourceValue(@Nullable String resourceValue) { if (resourceValue == null) { return grantedPermission -> true; } return grantedPermission -> { - Pattern value = grantedPermission.getValue(); - if (value == null) { + Pattern valuePattern = grantedPermission.getCompiledValuePattern(); + if (valuePattern == null) { return true; } - return value.matcher(resourceValue).matches(); + return valuePattern.matcher(resourceValue).matches(); }; } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ApplicationRestarter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ApplicationRestarter.java new file mode 100644 index 0000000000..42c8136f92 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ApplicationRestarter.java @@ -0,0 +1,46 @@ +package com.provectus.kafka.ui.util; + +import com.provectus.kafka.ui.KafkaUiApplication; +import java.io.Closeable; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class ApplicationRestarter implements ApplicationListener { + + private String[] applicationArgs; + private ApplicationContext applicationContext; + + @Override + public void onApplicationEvent(ApplicationStartedEvent event) { + this.applicationArgs = event.getArgs(); + this.applicationContext = event.getApplicationContext(); + } + + public void requestRestart() { + log.info("Restarting application"); + Thread thread = new Thread(() -> { + closeApplicationContext(applicationContext); + KafkaUiApplication.startApplication(applicationArgs); + }); + thread.setName("restartedMain-" + System.currentTimeMillis()); + thread.setDaemon(false); + thread.start(); + } + + private void closeApplicationContext(ApplicationContext context) { + while (context instanceof Closeable) { + try { + ((Closeable) context).close(); + } catch (Exception e) { + log.warn("Error stopping application before restart", e); + throw new RuntimeException(e); + } + context = context.getParent(); + } + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/DynamicConfigOperations.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/DynamicConfigOperations.java new file mode 100644 index 0000000000..2e1b32d3f1 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/DynamicConfigOperations.java @@ -0,0 +1,228 @@ +package com.provectus.kafka.ui.util; + + +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.config.auth.OAuthProperties; +import com.provectus.kafka.ui.config.auth.RoleBasedAccessControlProperties; +import com.provectus.kafka.ui.exception.FileUploadException; +import com.provectus.kafka.ui.exception.ValidationException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.Instant; +import java.util.Optional; +import javax.annotation.Nullable; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.env.YamlPropertySourceLoader; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.codec.multipart.FilePart; +import org.springframework.stereotype.Component; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.introspector.BeanAccess; +import org.yaml.snakeyaml.introspector.Property; +import org.yaml.snakeyaml.introspector.PropertyUtils; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; +import reactor.core.publisher.Mono; + +@Slf4j +@RequiredArgsConstructor +@Component +public class DynamicConfigOperations { + + static final String DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY = "dynamic.config.enabled"; + static final String DYNAMIC_CONFIG_PATH_ENV_PROPERTY = "dynamic.config.path"; + static final String DYNAMIC_CONFIG_PATH_ENV_PROPERTY_DEFAULT = "/etc/kafkaui/dynamic_config.yaml"; + + static final String CONFIG_RELATED_UPLOADS_DIR_PROPERTY = "config.related.uploads.dir"; + static final String CONFIG_RELATED_UPLOADS_DIR_DEFAULT = "/etc/kafkaui/uploads"; + + public static ApplicationContextInitializer dynamicConfigPropertiesInitializer() { + return appCtx -> + new DynamicConfigOperations(appCtx) + .loadDynamicPropertySource() + .ifPresent(source -> appCtx.getEnvironment().getPropertySources().addFirst(source)); + } + + private final ConfigurableApplicationContext ctx; + + public boolean dynamicConfigEnabled() { + return "true".equalsIgnoreCase(ctx.getEnvironment().getProperty(DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY)); + } + + private Path dynamicConfigFilePath() { + return Paths.get( + Optional.ofNullable(ctx.getEnvironment().getProperty(DYNAMIC_CONFIG_PATH_ENV_PROPERTY)) + .orElse(DYNAMIC_CONFIG_PATH_ENV_PROPERTY_DEFAULT) + ); + } + + @SneakyThrows + public Optional> loadDynamicPropertySource() { + if (dynamicConfigEnabled()) { + Path configPath = dynamicConfigFilePath(); + if (!Files.exists(configPath) || !Files.isReadable(configPath)) { + log.warn("Dynamic config file {} doesnt exist or not readable", configPath); + return Optional.empty(); + } + var propertySource = new CompositePropertySource("dynamicProperties"); + new YamlPropertySourceLoader() + .load("dynamicProperties", new FileSystemResource(configPath)) + .forEach(propertySource::addPropertySource); + log.info("Dynamic config loaded from {}", configPath); + return Optional.of(propertySource); + } + return Optional.empty(); + } + + public PropertiesStructure getCurrentProperties() { + return PropertiesStructure.builder() + .kafka(getNullableBean(ClustersProperties.class)) + .rbac(getNullableBean(RoleBasedAccessControlProperties.class)) + .auth( + PropertiesStructure.Auth.builder() + .type(ctx.getEnvironment().getProperty("auth.type")) + .oauth2(getNullableBean(OAuthProperties.class)) + .build()) + .build(); + } + + @Nullable + private T getNullableBean(Class clazz) { + try { + return ctx.getBean(clazz); + } catch (NoSuchBeanDefinitionException nsbde) { + return null; + } + } + + public void persist(PropertiesStructure properties) { + if (!dynamicConfigEnabled()) { + throw new ValidationException( + "Dynamic config change is not allowed. " + + "Set dynamic.config.enabled property to 'true' to enabled it."); + } + properties.initAndValidate(); + + String yaml = serializeToYaml(properties); + writeYamlToFile(yaml, dynamicConfigFilePath()); + } + + public Mono uploadConfigRelatedFile(FilePart file) { + String targetDirStr = (String) ctx.getEnvironment().getSystemEnvironment() + .getOrDefault(CONFIG_RELATED_UPLOADS_DIR_PROPERTY, CONFIG_RELATED_UPLOADS_DIR_DEFAULT); + + Path targetDir = Path.of(targetDirStr); + if (!Files.exists(targetDir)) { + try { + Files.createDirectories(targetDir); + } catch (IOException e) { + return Mono.error( + new FileUploadException("Error creating directory for uploads %s".formatted(targetDir), e)); + } + } + + Path targetFilePath = targetDir.resolve(file.filename() + "-" + Instant.now().getEpochSecond()); + log.info("Uploading config-related file {}", targetFilePath); + if (Files.exists(targetFilePath)) { + log.info("File {} already exists, it will be overwritten", targetFilePath); + } + + return file.transferTo(targetFilePath) + .thenReturn(targetFilePath) + .doOnError(th -> log.error("Error uploading file {}", targetFilePath, th)) + .onErrorMap(th -> new FileUploadException(targetFilePath, th)); + } + + @SneakyThrows + private void writeYamlToFile(String yaml, Path path) { + if (Files.isDirectory(path)) { + throw new ValidationException("Dynamic file path is a directory, but should be a file path"); + } + if (!Files.exists(path.getParent())) { + Files.createDirectories(path.getParent()); + } + if (Files.exists(path) && !Files.isWritable(path)) { + throw new ValidationException("File already exists and is not writable"); + } + try { + Files.writeString( + path, + yaml, + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING // to override existing file + ); + } catch (IOException e) { + throw new ValidationException("Error writing to " + path, e); + } + } + + private String serializeToYaml(PropertiesStructure props) { + //representer, that skips fields with null values + Representer representer = new Representer(new DumperOptions()) { + @Override + protected NodeTuple representJavaBeanProperty(Object javaBean, + Property property, + Object propertyValue, + Tag customTag) { + if (propertyValue == null) { + return null; // if value of property is null, ignore it. + } else { + return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); + } + } + }; + var propertyUtils = new PropertyUtils(); + propertyUtils.setBeanAccess(BeanAccess.FIELD); + representer.setPropertyUtils(propertyUtils); + representer.addClassTag(PropertiesStructure.class, Tag.MAP); //to avoid adding class tag + representer.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); //use indent instead of {} + return new Yaml(representer).dump(props); + } + + ///--------------------------------------------------------------------- + + @Data + @Builder + // field name should be in sync with @ConfigurationProperties annotation + public static class PropertiesStructure { + + private ClustersProperties kafka; + private RoleBasedAccessControlProperties rbac; + private Auth auth; + + @Data + @Builder + public static class Auth { + String type; + OAuthProperties oauth2; + } + + public void initAndValidate() { + Optional.ofNullable(kafka) + .ifPresent(ClustersProperties::validateAndSetDefaults); + + Optional.ofNullable(rbac) + .ifPresent(RoleBasedAccessControlProperties::init); + + Optional.ofNullable(auth) + .flatMap(a -> Optional.ofNullable(a.oauth2)) + .ifPresent(OAuthProperties::validate); + } + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/JmxPoolFactory.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/JmxPoolFactory.java deleted file mode 100644 index 49e73a58f2..0000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/JmxPoolFactory.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.provectus.kafka.ui.util; - -import com.provectus.kafka.ui.model.JmxConnectionInfo; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import javax.management.remote.JMXConnector; -import javax.management.remote.JMXConnectorFactory; -import javax.management.remote.JMXServiceURL; -import javax.rmi.ssl.SslRMIClientSocketFactory; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.pool2.BaseKeyedPooledObjectFactory; -import org.apache.commons.pool2.PooledObject; -import org.apache.commons.pool2.impl.DefaultPooledObject; - -@Slf4j -public class JmxPoolFactory extends BaseKeyedPooledObjectFactory { - - @Override - public JMXConnector create(JmxConnectionInfo info) throws Exception { - Map env = new HashMap<>(); - if (StringUtils.isNotEmpty(info.getUsername()) && StringUtils.isNotEmpty(info.getPassword())) { - env.put("jmx.remote.credentials", new String[] {info.getUsername(), info.getPassword()}); - } - - if (info.isSsl()) { - env.put("com.sun.jndi.rmi.factory.socket", new SslRMIClientSocketFactory()); - } - - return JMXConnectorFactory.connect(new JMXServiceURL(info.getUrl()), env); - } - - @Override - public PooledObject wrap(JMXConnector jmxConnector) { - return new DefaultPooledObject<>(jmxConnector); - } - - @Override - public void destroyObject(JmxConnectionInfo key, PooledObject p) { - try { - p.getObject().close(); - } catch (IOException e) { - log.error("Cannot close connection with {}", key); - } - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaServicesValidation.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaServicesValidation.java new file mode 100644 index 0000000000..7f5b8c45f2 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaServicesValidation.java @@ -0,0 +1,147 @@ +package com.provectus.kafka.ui.util; + +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.connect.api.KafkaConnectClientApi; +import com.provectus.kafka.ui.model.ApplicationPropertyValidationDTO; +import com.provectus.kafka.ui.service.ReactiveAdminClient; +import com.provectus.kafka.ui.service.ksql.KsqlApiClient; +import com.provectus.kafka.ui.sr.api.KafkaSrClientApi; +import java.io.FileInputStream; +import java.security.KeyStore; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.function.Supplier; +import javax.annotation.Nullable; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.AdminClientConfig; +import org.springframework.util.ResourceUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +@Slf4j +@UtilityClass +public class KafkaServicesValidation { + + private Mono valid() { + return Mono.just(new ApplicationPropertyValidationDTO().error(false)); + } + + private Mono invalid(String errorMsg) { + return Mono.just(new ApplicationPropertyValidationDTO().error(true).errorMessage(errorMsg)); + } + + private Mono invalid(Throwable th) { + return Mono.just(new ApplicationPropertyValidationDTO().error(true).errorMessage(th.getMessage())); + } + + /** + * Returns error msg, if any. + */ + public Optional validateTruststore(ClustersProperties.TruststoreConfig truststoreConfig) { + if (truststoreConfig.getTruststoreLocation() != null && truststoreConfig.getTruststorePassword() != null) { + try { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load( + new FileInputStream((ResourceUtils.getFile(truststoreConfig.getTruststoreLocation()))), + truststoreConfig.getTruststorePassword().toCharArray() + ); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm() + ); + trustManagerFactory.init(trustStore); + } catch (Exception e) { + return Optional.of(e.getMessage()); + } + } + return Optional.empty(); + } + + public Mono validateClusterConnection(String bootstrapServers, + Properties clusterProps, + @Nullable + ClustersProperties.TruststoreConfig ssl) { + Properties properties = new Properties(); + SslPropertiesUtil.addKafkaSslProperties(ssl, properties); + properties.putAll(clusterProps); + properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + // editing properties to make validation faster + properties.put(AdminClientConfig.RETRIES_CONFIG, 1); + properties.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, 5_000); + properties.put(AdminClientConfig.DEFAULT_API_TIMEOUT_MS_CONFIG, 5_000); + properties.put(AdminClientConfig.CLIENT_ID_CONFIG, "kui-admin-client-validation-" + System.currentTimeMillis()); + AdminClient adminClient = null; + try { + adminClient = AdminClient.create(properties); + } catch (Exception e) { + log.error("Error creating admin client during validation", e); + return invalid("Error while creating AdminClient. See logs for details."); + } + return Mono.just(adminClient) + .then(ReactiveAdminClient.toMono(adminClient.listTopics().names())) + .then(valid()) + .doOnTerminate(adminClient::close) + .onErrorResume(th -> { + log.error("Error connecting to cluster", th); + return KafkaServicesValidation.invalid("Error connecting to cluster. See logs for details."); + }); + } + + public Mono validateSchemaRegistry( + Supplier> clientSupplier) { + ReactiveFailover client; + try { + client = clientSupplier.get(); + } catch (Exception e) { + log.error("Error creating Schema Registry client", e); + return invalid("Error creating Schema Registry client: " + e.getMessage()); + } + return client + .mono(KafkaSrClientApi::getGlobalCompatibilityLevel) + .then(valid()) + .onErrorResume(KafkaServicesValidation::invalid); + } + + public Mono validateConnect( + Supplier> clientSupplier) { + ReactiveFailover client; + try { + client = clientSupplier.get(); + } catch (Exception e) { + log.error("Error creating Connect client", e); + return invalid("Error creating Connect client: " + e.getMessage()); + } + return client.flux(KafkaConnectClientApi::getConnectorPlugins) + .collectList() + .then(valid()) + .onErrorResume(KafkaServicesValidation::invalid); + } + + public Mono validateKsql(Supplier> clientSupplier) { + ReactiveFailover client; + try { + client = clientSupplier.get(); + } catch (Exception e) { + log.error("Error creating Ksql client", e); + return invalid("Error creating Ksql client: " + e.getMessage()); + } + return client.flux(c -> c.execute("SHOW VARIABLES;", Map.of())) + .collectList() + .flatMap(ksqlResults -> + Flux.fromIterable(ksqlResults) + .filter(KsqlApiClient.KsqlResponseTable::isError) + .flatMap(err -> invalid("Error response from ksql: " + err)) + .next() + .switchIfEmpty(valid()) + ) + .onErrorResume(KafkaServicesValidation::invalid); + } + + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/NumberUtil.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaVersion.java similarity index 74% rename from kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/NumberUtil.java rename to kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaVersion.java index 7237f1b886..48ff7ff121 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/NumberUtil.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaVersion.java @@ -1,15 +1,13 @@ package com.provectus.kafka.ui.util; +import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; +@UtilityClass @Slf4j -public class NumberUtil { +public class KafkaVersion { - private NumberUtil() { - } - - - public static float parserClusterVersion(String version) throws NumberFormatException { + public static float parse(String version) throws NumberFormatException { log.trace("Parsing cluster version [{}]", version); try { final String[] parts = version.split("\\."); @@ -22,4 +20,4 @@ public class NumberUtil { throw e; } } -} \ No newline at end of file +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/MapUtil.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/MapUtil.java deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/PollingThrottler.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/PollingThrottler.java index 213f0fdbd7..bd2e97da97 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/PollingThrottler.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/PollingThrottler.java @@ -3,8 +3,6 @@ package com.provectus.kafka.ui.util; import com.google.common.annotations.VisibleForTesting; import com.google.common.util.concurrent.RateLimiter; import com.provectus.kafka.ui.config.ClustersProperties; -import com.provectus.kafka.ui.model.KafkaCluster; -import java.util.Optional; import java.util.function.Supplier; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.consumer.ConsumerRecords; @@ -14,8 +12,8 @@ import org.apache.kafka.common.utils.Bytes; public class PollingThrottler { public static Supplier throttlerSupplier(ClustersProperties.Cluster cluster) { - long rate = cluster.getPollingThrottleRate(); - if (rate <= 0) { + Long rate = cluster.getPollingThrottleRate(); + if (rate == null || rate <= 0) { return PollingThrottler::noop; } // RateLimiter instance should be shared across all created throttlers diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ReactiveFailover.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ReactiveFailover.java index 1066dc7178..0293e4f925 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ReactiveFailover.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ReactiveFailover.java @@ -9,7 +9,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; -import org.springframework.web.reactive.function.client.WebClientRequestException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -25,6 +24,16 @@ public class ReactiveFailover { private final Predicate failoverExceptionsPredicate; private final String noAvailablePublishersMsg; + // creates single-publisher failover (basically for tests usage) + public static ReactiveFailover createNoop(T publisher) { + return create( + List.of(publisher), + th -> true, + "publisher is not available", + DEFAULT_RETRY_GRACE_PERIOD_MS + ); + } + public static ReactiveFailover create(List publishers, Predicate failoverExeptionsPredicate, String noAvailablePublishersMsg, @@ -68,7 +77,8 @@ public class ReactiveFailover { private Mono mono(Function> f, List> candidates) { var publisher = candidates.get(0); - return f.apply(publisher.get()) + return publisher.get() + .flatMap(f) .onErrorResume(failoverExceptionsPredicate, th -> { publisher.markFailed(); if (candidates.size() == 1) { @@ -92,7 +102,8 @@ public class ReactiveFailover { private Flux flux(Function> f, List> candidates) { var publisher = candidates.get(0); - return f.apply(publisher.get()) + return publisher.get() + .flatMapMany(f) .onErrorResume(failoverExceptionsPredicate, th -> { publisher.markFailed(); if (candidates.size() == 1) { @@ -135,11 +146,15 @@ public class ReactiveFailover { this.retryGracePeriodMs = retryGracePeriodMs; } - synchronized T get() { + synchronized Mono get() { if (publisherInstance == null) { - publisherInstance = supplier.get(); + try { + publisherInstance = supplier.get(); + } catch (Throwable th) { + return Mono.error(th); + } } - return publisherInstance; + return Mono.just(publisherInstance); } void markFailed() { diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/SslPropertiesUtil.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/SslPropertiesUtil.java new file mode 100644 index 0000000000..ea12007637 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/SslPropertiesUtil.java @@ -0,0 +1,33 @@ +package com.provectus.kafka.ui.util; + +import com.provectus.kafka.ui.config.ClustersProperties; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import java.io.FileInputStream; +import java.security.KeyStore; +import java.util.Properties; +import javax.annotation.Nullable; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; +import org.apache.kafka.common.config.SslConfigs; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.util.ResourceUtils; +import reactor.netty.http.client.HttpClient; + +@UtilityClass +public class SslPropertiesUtil { + + public void addKafkaSslProperties(@Nullable ClustersProperties.TruststoreConfig truststoreConfig, + Properties sink) { + if (truststoreConfig != null && truststoreConfig.getTruststoreLocation() != null) { + sink.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, truststoreConfig.getTruststoreLocation()); + if (truststoreConfig.getTruststorePassword() != null) { + sink.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, truststoreConfig.getTruststorePassword()); + } + } + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/WebClientConfigurator.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/WebClientConfigurator.java index f07529530d..fe2240bd6a 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/WebClientConfigurator.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/WebClientConfigurator.java @@ -5,8 +5,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.provectus.kafka.ui.config.ClustersProperties; import com.provectus.kafka.ui.exception.ValidationException; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.ssl.JdkSslContext; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; import java.io.FileInputStream; import java.security.KeyStore; import java.util.function.Consumer; @@ -40,48 +43,43 @@ public class WebClientConfigurator { .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } - - public WebClientConfigurator configureSsl(@Nullable ClustersProperties.WebClientSsl ssl) { - if (ssl != null) { - return configureSsl( - ssl.getKeystoreLocation(), - ssl.getKeystorePassword(), - ssl.getTruststoreLocation(), - ssl.getTruststorePassword() - ); - } - return this; + public WebClientConfigurator configureSsl(@Nullable ClustersProperties.TruststoreConfig truststoreConfig, + @Nullable ClustersProperties.KeystoreConfig keystoreConfig) { + return configureSsl( + keystoreConfig != null ? keystoreConfig.getKeystoreLocation() : null, + keystoreConfig != null ? keystoreConfig.getKeystorePassword() : null, + truststoreConfig != null ? truststoreConfig.getTruststoreLocation() : null, + truststoreConfig != null ? truststoreConfig.getTruststorePassword() : null + ); } @SneakyThrows - public WebClientConfigurator configureSsl( + private WebClientConfigurator configureSsl( @Nullable String keystoreLocation, @Nullable String keystorePassword, @Nullable String truststoreLocation, @Nullable String truststorePassword) { - // If we want to customize our TLS configuration, we need at least a truststore - if (truststoreLocation == null || truststorePassword == null) { + if (truststoreLocation == null && keystoreLocation == null) { return this; } SslContextBuilder contextBuilder = SslContextBuilder.forClient(); - - // Prepare truststore - KeyStore trustStore = KeyStore.getInstance("JKS"); - trustStore.load( - new FileInputStream((ResourceUtils.getFile(truststoreLocation))), - truststorePassword.toCharArray() - ); - - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm() - ); - trustManagerFactory.init(trustStore); - contextBuilder.trustManager(trustManagerFactory); + if (truststoreLocation != null && truststorePassword != null) { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load( + new FileInputStream((ResourceUtils.getFile(truststoreLocation))), + truststorePassword.toCharArray() + ); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm() + ); + trustManagerFactory.init(trustStore); + contextBuilder.trustManager(trustManagerFactory); + } // Prepare keystore only if we got a keystore if (keystoreLocation != null && keystorePassword != null) { - KeyStore keyStore = KeyStore.getInstance("JKS"); + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load( new FileInputStream(ResourceUtils.getFile(keystoreLocation)), keystorePassword.toCharArray() diff --git a/kafka-ui-api/src/main/resources/application-local.yml b/kafka-ui-api/src/main/resources/application-local.yml index 5eaeee10b4..5eb3772e2b 100644 --- a/kafka-ui-api/src/main/resources/application-local.yml +++ b/kafka-ui-api/src/main/resources/application-local.yml @@ -1,3 +1,11 @@ +logging: + level: + root: INFO + com.provectus: DEBUG + #org.springframework.http.codec.json.Jackson2JsonEncoder: DEBUG + #org.springframework.http.codec.json.Jackson2JsonDecoder: DEBUG + reactor.netty.http.server.AccessLog: INFO + kafka: clusters: - name: local @@ -57,4 +65,4 @@ auth: roles.file: /tmp/roles.yml #server: -# port: 8080 #- Port in which kafka-ui will run. \ No newline at end of file +# port: 8080 #- Port in which kafka-ui will run. diff --git a/kafka-ui-api/src/main/resources/application.yml b/kafka-ui-api/src/main/resources/application.yml index da070a19a8..864289c2d9 100644 --- a/kafka-ui-api/src/main/resources/application.yml +++ b/kafka-ui-api/src/main/resources/application.yml @@ -16,7 +16,5 @@ logging: level: root: INFO com.provectus: DEBUG - #org.springframework.http.codec.json.Jackson2JsonEncoder: DEBUG - #org.springframework.http.codec.json.Jackson2JsonDecoder: DEBUG reactor.netty.http.server.AccessLog: INFO diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConnectServiceTests.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConnectServiceTests.java index f71149d3d8..a827183521 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConnectServiceTests.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConnectServiceTests.java @@ -142,9 +142,8 @@ public class KafkaConnectServiceTests extends AbstractIntegrationTest { .uri("/api/clusters/{clusterName}/connects/{connectName}/connectors", LOCAL, connectName) .exchange() .expectStatus().isOk() - .expectBody() - .jsonPath(String.format("$[?(@ == '%s')]", connectorName)) - .exists(); + .expectBodyList(String.class) + .contains(connectorName); } @Test diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/AvroEmbeddedSerdeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/AvroEmbeddedSerdeTest.java new file mode 100644 index 0000000000..2f4734ce06 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/AvroEmbeddedSerdeTest.java @@ -0,0 +1,92 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.PropertyResolverImpl; +import io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.apache.avro.Schema; +import org.apache.avro.file.DataFileWriter; +import org.apache.avro.generic.GenericData; +import org.apache.avro.generic.GenericDatumWriter; +import org.apache.avro.generic.GenericRecord; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class AvroEmbeddedSerdeTest { + + private AvroEmbeddedSerde avroEmbeddedSerde; + + @BeforeEach + void init() { + avroEmbeddedSerde = new AvroEmbeddedSerde(); + avroEmbeddedSerde.configure( + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty() + ); + } + + @ParameterizedTest + @EnumSource + void canDeserializeReturnsTrueForAllTargets(Serde.Target target) { + assertThat(avroEmbeddedSerde.canDeserialize("anyTopic", target)) + .isTrue(); + } + + @ParameterizedTest + @EnumSource + void canSerializeReturnsFalseForAllTargets(Serde.Target target) { + assertThat(avroEmbeddedSerde.canSerialize("anyTopic", target)) + .isFalse(); + } + + @Test + void deserializerParsesAvroDataWithEmbeddedSchema() throws Exception { + Schema schema = new Schema.Parser().parse(""" + { + "type": "record", + "name": "TestAvroRecord", + "fields": [ + { "name": "field1", "type": "string" }, + { "name": "field2", "type": "int" } + ] + } + """ + ); + GenericRecord record = new GenericData.Record(schema); + record.put("field1", "this is test msg"); + record.put("field2", 100500); + + String jsonRecord = new String(AvroSchemaUtils.toJson(record)); + byte[] serializedRecordBytes = serializeAvroWithEmbeddedSchema(record); + + var deserializer = avroEmbeddedSerde.deserializer("anyTopic", Serde.Target.KEY); + DeserializeResult result = deserializer.deserialize(null, serializedRecordBytes); + assertThat(result.getType()).isEqualTo(DeserializeResult.Type.JSON); + assertThat(result.getAdditionalProperties()).isEmpty(); + assertJsonEquals(jsonRecord, result.getResult()); + } + + private void assertJsonEquals(String expected, String actual) throws IOException { + var mapper = new JsonMapper(); + assertThat(mapper.readTree(actual)).isEqualTo(mapper.readTree(expected)); + } + + private byte[] serializeAvroWithEmbeddedSchema(GenericRecord record) throws IOException { + try (DataFileWriter writer = new DataFileWriter<>(new GenericDatumWriter<>()); + ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + writer.create(record.getSchema(), baos); + writer.append(record); + writer.flush(); + return baos.toByteArray(); + } + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerdeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerdeTest.java index 3de915145f..ab99df74de 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerdeTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerdeTest.java @@ -10,14 +10,16 @@ import com.google.protobuf.Descriptors; import com.google.protobuf.util.JsonFormat; import com.provectus.kafka.ui.serde.api.PropertyResolver; import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.builtin.ProtobufFileSerde.Configuration; +import com.squareup.wire.schema.ProtoFile; import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; -import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Optional; import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.util.ResourceUtils; @@ -29,28 +31,29 @@ class ProtobufFileSerdeTest { private static final String sampleBookMsgJson = "{\"version\": 1, \"people\": [" + "{ \"name\": \"My Name\",\"id\": 102, \"email\": \"addrBook@example.com\", \"phones\":[]}]}"; - private static final String sampleSensorMsgJson = "{ \"name\": \"My Sensor\", " - + "\"temperature\": 20.5, \"humidity\": 50, \"door\": \"OPEN\" }"; + private static final String sampleLangDescriptionMsgJson = "{ \"lang\": \"EN\", " + + "\"descr\": \"Some description here\" }"; // Sample message of type `test.Person` private byte[] personMessageBytes; // Sample message of type `test.AddressBook` private byte[] addressBookMessageBytes; - private byte[] sensorMessageBytes; - private Path addressBookSchemaPath; - private Path sensorSchemaPath; - + private byte[] langDescriptionMessageBytes; private Descriptors.Descriptor personDescriptor; private Descriptors.Descriptor addressBookDescriptor; - private Descriptors.Descriptor sensorDescriptor; + private Descriptors.Descriptor langDescriptionDescriptor; private Map descriptorPaths; @BeforeEach void setUp() throws Exception { - addressBookSchemaPath = ResourceUtils.getFile("classpath:address-book.proto").toPath(); - sensorSchemaPath = ResourceUtils.getFile("classpath:sensor.proto").toPath(); + Map files = ProtobufFileSerde.Configuration.loadSchemas( + Optional.empty(), + Optional.empty(), + Optional.of(protoFilesDir()) + ); - ProtobufSchema addressBookSchema = new ProtobufSchema(Files.readString(addressBookSchemaPath)); + Path addressBookSchemaPath = ResourceUtils.getFile("classpath:protobuf-serde/address-book.proto").toPath(); + var addressBookSchema = files.get(addressBookSchemaPath); var builder = addressBookSchema.newMessageBuilder("test.Person"); JsonFormat.parser().merge(samplePersonMsgJson, builder); personMessageBytes = builder.build().toByteArray(); @@ -61,63 +64,241 @@ class ProtobufFileSerdeTest { personDescriptor = addressBookSchema.toDescriptor("test.Person"); addressBookDescriptor = addressBookSchema.toDescriptor("test.AddressBook"); - ProtobufSchema sensorSchema = new ProtobufSchema(Files.readString(sensorSchemaPath)); - builder = sensorSchema.newMessageBuilder("iot.Sensor"); - JsonFormat.parser().merge(sampleSensorMsgJson, builder); - sensorMessageBytes = builder.build().toByteArray(); - sensorDescriptor = sensorSchema.toDescriptor("iot.Sensor"); + Path languageDescriptionPath = ResourceUtils.getFile("classpath:protobuf-serde/lang-description.proto").toPath(); + var languageDescriptionSchema = files.get(languageDescriptionPath); + builder = languageDescriptionSchema.newMessageBuilder("test.LanguageDescription"); + JsonFormat.parser().merge(sampleLangDescriptionMsgJson, builder); + langDescriptionMessageBytes = builder.build().toByteArray(); + langDescriptionDescriptor = languageDescriptionSchema.toDescriptor("test.LanguageDescription"); descriptorPaths = Map.of( personDescriptor, addressBookSchemaPath, - addressBookDescriptor, addressBookSchemaPath, - sensorDescriptor, sensorSchemaPath + addressBookDescriptor, addressBookSchemaPath ); } - @Test - void testDeserialize() { - var messageNameMap = Map.of( - "persons", personDescriptor, - "books", addressBookDescriptor - ); - var keyMessageNameMap = Map.of( - "books", addressBookDescriptor); + void loadsAllProtoFiledFromTargetDirectory() throws Exception { + var protoDir = ResourceUtils.getFile("classpath:protobuf-serde/").getPath(); + List files = new ProtobufFileSerde.ProtoSchemaLoader(protoDir).load(); + assertThat(files).hasSize(4); + assertThat(files) + .map(f -> f.getLocation().getPath()) + .containsExactlyInAnyOrder( + "language/language.proto", + "sensor.proto", + "address-book.proto", + "lang-description.proto" + ); + } - var serde = new ProtobufFileSerde(); - serde.configure( - null, - null, - descriptorPaths, - messageNameMap, - keyMessageNameMap - ); + @SneakyThrows + private String protoFilesDir() { + return ResourceUtils.getFile("classpath:protobuf-serde/").getPath(); + } - var deserializedPerson = serde.deserializer("persons", Serde.Target.VALUE) - .deserialize(null, personMessageBytes); - assertJsonEquals(samplePersonMsgJson, deserializedPerson.getResult()); + @Nested + class ConfigurationTests { - var deserializedBook = serde.deserializer("books", Serde.Target.KEY) - .deserialize(null, addressBookMessageBytes); - assertJsonEquals(sampleBookMsgJson, deserializedBook.getResult()); + @Test + void canBeAutoConfiguredReturnsNoProtoPropertiesProvided() { + PropertyResolver resolver = mock(PropertyResolver.class); + assertThat(Configuration.canBeAutoConfigured(resolver)) + .isFalse(); + } + + @Test + void canBeAutoConfiguredReturnsTrueIfNoProtoFileHasBeenProvided() { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getProperty("protobufFile", String.class)) + .thenReturn(Optional.of("file.proto")); + assertThat(Configuration.canBeAutoConfigured(resolver)) + .isTrue(); + } + + @Test + void canBeAutoConfiguredReturnsTrueIfProtoFilesHasBeenProvided() { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getListProperty("protobufFiles", String.class)) + .thenReturn(Optional.of(List.of("file.proto"))); + assertThat(Configuration.canBeAutoConfigured(resolver)) + .isTrue(); + } + + @Test + void canBeAutoConfiguredReturnsTrueIfProtoFilesDirProvided() { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getProperty("protobufFilesDir", String.class)) + .thenReturn(Optional.of("/filesDir")); + assertThat(Configuration.canBeAutoConfigured(resolver)) + .isTrue(); + } + + @Test + void unknownSchemaAsDefaultThrowsException() { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getProperty("protobufFilesDir", String.class)) + .thenReturn(Optional.of(protoFilesDir())); + + when(resolver.getProperty("protobufMessageName", String.class)) + .thenReturn(Optional.of("test.NotExistent")); + + assertThatThrownBy(() -> Configuration.create(resolver)) + .isInstanceOf(NullPointerException.class) + .hasMessage("The given message type not found in protobuf definition: test.NotExistent"); + } + + @Test + void unknownSchemaAsDefaultForKeyThrowsException() { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getProperty("protobufFilesDir", String.class)) + .thenReturn(Optional.of(protoFilesDir())); + + when(resolver.getProperty("protobufMessageNameForKey", String.class)) + .thenReturn(Optional.of("test.NotExistent")); + + assertThatThrownBy(() -> Configuration.create(resolver)) + .isInstanceOf(NullPointerException.class) + .hasMessage("The given message type not found in protobuf definition: test.NotExistent"); + } + + @Test + void unknownSchemaAsTopicSchemaThrowsException() { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getProperty("protobufFilesDir", String.class)) + .thenReturn(Optional.of(protoFilesDir())); + + when(resolver.getMapProperty("protobufMessageNameByTopic", String.class, String.class)) + .thenReturn(Optional.of(Map.of("persons", "test.NotExistent"))); + + assertThatThrownBy(() -> Configuration.create(resolver)) + .isInstanceOf(NullPointerException.class) + .hasMessage("The given message type not found in protobuf definition: test.NotExistent"); + } + + @Test + void unknownSchemaAsTopicSchemaForKeyThrowsException() { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getProperty("protobufFilesDir", String.class)) + .thenReturn(Optional.of(protoFilesDir())); + + when(resolver.getMapProperty("protobufMessageNameForKeyByTopic", String.class, String.class)) + .thenReturn(Optional.of(Map.of("persons", "test.NotExistent"))); + + assertThatThrownBy(() -> Configuration.create(resolver)) + .isInstanceOf(NullPointerException.class) + .hasMessage("The given message type not found in protobuf definition: test.NotExistent"); + } + + @Test + void createConfigureFillsDescriptorMappingsWhenProtoFilesListProvided() throws Exception { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getProperty("protobufFile", String.class)) + .thenReturn(Optional.of( + ResourceUtils.getFile("classpath:protobuf-serde/sensor.proto").getPath())); + + when(resolver.getListProperty("protobufFiles", String.class)) + .thenReturn(Optional.of( + List.of( + ResourceUtils.getFile("classpath:protobuf-serde/address-book.proto").getPath()))); + + when(resolver.getProperty("protobufMessageName", String.class)) + .thenReturn(Optional.of("test.Sensor")); + + when(resolver.getProperty("protobufMessageNameForKey", String.class)) + .thenReturn(Optional.of("test.AddressBook")); + + when(resolver.getMapProperty("protobufMessageNameByTopic", String.class, String.class)) + .thenReturn(Optional.of( + Map.of( + "topic1", "test.Sensor", + "topic2", "test.AddressBook"))); + + when(resolver.getMapProperty("protobufMessageNameForKeyByTopic", String.class, String.class)) + .thenReturn(Optional.of( + Map.of( + "topic1", "test.Person", + "topic2", "test.AnotherPerson"))); + + var configuration = Configuration.create(resolver); + + assertThat(configuration.defaultMessageDescriptor()) + .matches(d -> d.getFullName().equals("test.Sensor")); + assertThat(configuration.defaultKeyMessageDescriptor()) + .matches(d -> d.getFullName().equals("test.AddressBook")); + + assertThat(configuration.messageDescriptorMap()) + .containsOnlyKeys("topic1", "topic2") + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.Sensor")) + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.AddressBook")); + + assertThat(configuration.keyMessageDescriptorMap()) + .containsOnlyKeys("topic1", "topic2") + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.Person")) + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.AnotherPerson")); + } + + @Test + void createConfigureFillsDescriptorMappingsWhenProtoFileDirProvided() throws Exception { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getProperty("protobufFilesDir", String.class)) + .thenReturn(Optional.of(protoFilesDir())); + + when(resolver.getProperty("protobufMessageName", String.class)) + .thenReturn(Optional.of("test.Sensor")); + + when(resolver.getProperty("protobufMessageNameForKey", String.class)) + .thenReturn(Optional.of("test.AddressBook")); + + when(resolver.getMapProperty("protobufMessageNameByTopic", String.class, String.class)) + .thenReturn(Optional.of( + Map.of( + "topic1", "test.Sensor", + "topic2", "test.LanguageDescription"))); + + when(resolver.getMapProperty("protobufMessageNameForKeyByTopic", String.class, String.class)) + .thenReturn(Optional.of( + Map.of( + "topic1", "test.Person", + "topic2", "test.AnotherPerson"))); + + var configuration = Configuration.create(resolver); + + assertThat(configuration.defaultMessageDescriptor()) + .matches(d -> d.getFullName().equals("test.Sensor")); + assertThat(configuration.defaultKeyMessageDescriptor()) + .matches(d -> d.getFullName().equals("test.AddressBook")); + + assertThat(configuration.messageDescriptorMap()) + .containsOnlyKeys("topic1", "topic2") + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.Sensor")) + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.LanguageDescription")); + + assertThat(configuration.keyMessageDescriptorMap()) + .containsOnlyKeys("topic1", "topic2") + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.Person")) + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.AnotherPerson")); + } } @Test - void testDeserializeMultipleProtobuf() { + void deserializeUsesTopicsMappingToFindMsgDescriptor() { var messageNameMap = Map.of( "persons", personDescriptor, "books", addressBookDescriptor, - "sensors", sensorDescriptor + "langs", langDescriptionDescriptor ); var keyMessageNameMap = Map.of( "books", addressBookDescriptor); var serde = new ProtobufFileSerde(); serde.configure( - null, - null, - descriptorPaths, - messageNameMap, - keyMessageNameMap + new Configuration( + null, + null, + descriptorPaths, + messageNameMap, + keyMessageNameMap + ) ); var deserializedPerson = serde.deserializer("persons", Serde.Target.VALUE) @@ -128,20 +309,22 @@ class ProtobufFileSerdeTest { .deserialize(null, addressBookMessageBytes); assertJsonEquals(sampleBookMsgJson, deserializedBook.getResult()); - var deserializedSensor = serde.deserializer("sensors", Serde.Target.VALUE) - .deserialize(null, sensorMessageBytes); - assertJsonEquals(sampleSensorMsgJson, deserializedSensor.getResult()); + var deserializedSensor = serde.deserializer("langs", Serde.Target.VALUE) + .deserialize(null, langDescriptionMessageBytes); + assertJsonEquals(sampleLangDescriptionMsgJson, deserializedSensor.getResult()); } @Test - void testDefaultMessageName() { + void deserializeUsesDefaultDescriptorIfTopicMappingNotFound() { var serde = new ProtobufFileSerde(); serde.configure( - personDescriptor, - addressBookDescriptor, - descriptorPaths, - Map.of(), - Map.of() + new Configuration( + personDescriptor, + addressBookDescriptor, + descriptorPaths, + Map.of(), + Map.of() + ) ); var deserializedPerson = serde.deserializer("persons", Serde.Target.VALUE) @@ -154,230 +337,57 @@ class ProtobufFileSerdeTest { } @Test - void testSerialize() { - var messageNameMap = Map.of( - "persons", personDescriptor, - "books", addressBookDescriptor - ); - var keyMessageNameMap = Map.of( - "books", addressBookDescriptor); - - var serde = new ProtobufFileSerde(); - serde.configure( - null, - null, - descriptorPaths, - messageNameMap, - keyMessageNameMap - ); - - var personBytes = serde.serializer("persons", Serde.Target.VALUE) - .serialize("{ \"name\": \"My Name\",\"id\": 101, \"email\": \"user1@example.com\" }"); - assertThat(personBytes).isEqualTo(personMessageBytes); - - var booksBytes = serde.serializer("books", Serde.Target.KEY) - .serialize("{\"version\": 1, \"people\": [" - + "{ \"name\": \"My Name\",\"id\": 102, \"email\": \"addrBook@example.com\" }]}"); - assertThat(booksBytes).isEqualTo(addressBookMessageBytes); - } - - @Test - void testSerializeMultipleProtobuf() { + void serializeUsesTopicsMappingToFindMsgDescriptor() { var messageNameMap = Map.of( "persons", personDescriptor, "books", addressBookDescriptor, - "sensors", sensorDescriptor + "langs", langDescriptionDescriptor ); var keyMessageNameMap = Map.of( "books", addressBookDescriptor); var serde = new ProtobufFileSerde(); serde.configure( - null, - null, - descriptorPaths, - messageNameMap, - keyMessageNameMap + new Configuration( + null, + null, + descriptorPaths, + messageNameMap, + keyMessageNameMap + ) ); - var personBytes = serde.serializer("persons", Serde.Target.VALUE) - .serialize("{ \"name\": \"My Name\",\"id\": 101, \"email\": \"user1@example.com\" }"); - assertThat(personBytes).isEqualTo(personMessageBytes); + var personBytes = serde.serializer("langs", Serde.Target.VALUE) + .serialize(sampleLangDescriptionMsgJson); + assertThat(personBytes).isEqualTo(langDescriptionMessageBytes); var booksBytes = serde.serializer("books", Serde.Target.KEY) - .serialize("{\"version\": 1, \"people\": [" - + "{ \"name\": \"My Name\",\"id\": 102, \"email\": \"addrBook@example.com\" }]}"); + .serialize(sampleBookMsgJson); assertThat(booksBytes).isEqualTo(addressBookMessageBytes); - - var sensorBytes = serde.serializer("sensors", Serde.Target.VALUE) - .serialize("{ \"name\": \"My Sensor\", \"temperature\": 20.5, \"humidity\": 50, \"door\": \"OPEN\" }"); - assertThat(sensorBytes).isEqualTo(sensorMessageBytes); } @Test - void testSerializeDefaults() { + void serializeUsesDefaultDescriptorIfTopicMappingNotFound() { var serde = new ProtobufFileSerde(); serde.configure( - personDescriptor, - addressBookDescriptor, - descriptorPaths, - Map.of(), - Map.of() + new Configuration( + personDescriptor, + addressBookDescriptor, + descriptorPaths, + Map.of(), + Map.of() + ) ); var personBytes = serde.serializer("persons", Serde.Target.VALUE) - .serialize("{ \"name\": \"My Name\",\"id\": 101, \"email\": \"user1@example.com\" }"); + .serialize(samplePersonMsgJson); assertThat(personBytes).isEqualTo(personMessageBytes); var booksBytes = serde.serializer("books", Serde.Target.KEY) - .serialize("{\"version\": 1, \"people\": [" - + "{ \"name\": \"My Name\",\"id\": 102, \"email\": \"addrBook@example.com\" }]}"); + .serialize(sampleBookMsgJson); assertThat(booksBytes).isEqualTo(addressBookMessageBytes); } - @Test - void canBeAutoConfiguredReturnsFalseIfNoProtoFilesHaveBeenProvided() { - PropertyResolver resolver = mock(PropertyResolver.class); - - var serde = new ProtobufFileSerde(); - boolean startupSuccessful = serde.canBeAutoConfigured(resolver, resolver); - assertThat(startupSuccessful).isFalse(); - } - - @Test - void canBeAutoConfiguredReturnsFalseIfProtoFilesListIsEmpty() { - PropertyResolver resolver = mock(PropertyResolver.class); - when(resolver.getListProperty("protobufFiles", String.class)).thenReturn(Optional.of(List.of())); - - var serde = new ProtobufFileSerde(); - boolean startupSuccessful = serde.canBeAutoConfigured(resolver, resolver); - assertThat(startupSuccessful).isFalse(); - } - - @Test - void canBeAutoConfiguredReturnsTrueIfNoProtoFileHasBeenProvided() { - PropertyResolver resolver = mock(PropertyResolver.class); - when(resolver.getProperty("protobufFile", String.class)).thenReturn(Optional.of("file.proto")); - - var serde = new ProtobufFileSerde(); - boolean startupSuccessful = serde.canBeAutoConfigured(resolver, resolver); - assertThat(startupSuccessful).isTrue(); - } - - @Test - void canBeAutoConfiguredReturnsTrueIfProtoFilesHasBeenProvided() { - PropertyResolver resolver = mock(PropertyResolver.class); - when(resolver.getListProperty("protobufFiles", String.class)).thenReturn(Optional.of(List.of("file.proto"))); - - var serde = new ProtobufFileSerde(); - boolean startupSuccessful = serde.canBeAutoConfigured(resolver, resolver); - assertThat(startupSuccessful).isTrue(); - } - - @Test - void canBeAutoConfiguredReturnsTrueIfProtoFileAndProtoFilesHaveBeenProvided() { - PropertyResolver resolver = mock(PropertyResolver.class); - when(resolver.getProperty("protobufFile", String.class)).thenReturn(Optional.of("file1.proto")); - when(resolver.getListProperty("protobufFiles", String.class)).thenReturn(Optional.of(List.of("file2.proto"))); - - var serde = new ProtobufFileSerde(); - boolean startupSuccessful = serde.canBeAutoConfigured(resolver, resolver); - assertThat(startupSuccessful).isTrue(); - } - - @Test - void listOfProtobufFilesIsJoined() { - PropertyResolver resolver = mock(PropertyResolver.class); - when(resolver.getProperty("protobufFile", String.class)) - .thenReturn(Optional.of(addressBookSchemaPath.toString())); - when(resolver.getListProperty("protobufFiles", String.class)) - .thenReturn(Optional.of(List.of(sensorSchemaPath.toString()))); - when(resolver.getProperty("protobufMessageName", String.class)) - .thenReturn(Optional.of("test.AddressBook")); - - Map protobufMessageNameByTopic = Map.of( - "persons", "test.Person", - "books", "test.AddressBook", - "sensors", "iot.Sensor"); - when(resolver.getMapProperty("protobufMessageNameByTopic", String.class, String.class)) - .thenReturn(Optional.of(protobufMessageNameByTopic)); - - var serde = new ProtobufFileSerde(); - serde.configure(resolver, resolver, resolver); - - var deserializedPerson = serde.deserializer("persons", Serde.Target.VALUE) - .deserialize(null, personMessageBytes); - assertJsonEquals(samplePersonMsgJson, deserializedPerson.getResult()); - - var deserializedSensor = serde.deserializer("sensors", Serde.Target.VALUE) - .deserialize(null, sensorMessageBytes); - assertJsonEquals(sampleSensorMsgJson, deserializedSensor.getResult()); - } - - @Test - void unknownSchemaAsDefaultThrowsException() { - PropertyResolver resolver = mock(PropertyResolver.class); - when(resolver.getListProperty("protobufFiles", String.class)) - .thenReturn(Optional.of(List.of(addressBookSchemaPath.toString(), sensorSchemaPath.toString()))); - when(resolver.getProperty("protobufMessageName", String.class)) - .thenReturn(Optional.of("test.NotExistent")); - - var serde = new ProtobufFileSerde(); - assertThatThrownBy(() -> serde.configure(resolver, resolver, resolver)) - .isInstanceOf(NullPointerException.class) - .hasMessage("The given message type not found in protobuf definition: test.NotExistent"); - } - - @Test - void unknownSchemaAsDefaultForKeyThrowsException() { - PropertyResolver resolver = mock(PropertyResolver.class); - when(resolver.getListProperty("protobufFiles", String.class)) - .thenReturn(Optional.of(List.of(addressBookSchemaPath.toString(), sensorSchemaPath.toString()))); - when(resolver.getProperty("protobufMessageName", String.class)) - .thenReturn(Optional.of("test.AddressBook")); - when(resolver.getProperty("protobufMessageNameForKey", String.class)) - .thenReturn(Optional.of("test.NotExistent")); - - var serde = new ProtobufFileSerde(); - assertThatThrownBy(() -> serde.configure(resolver, resolver, resolver)) - .isInstanceOf(NullPointerException.class) - .hasMessage("The given message type not found in protobuf definition: test.NotExistent"); - } - - @Test - void unknownSchemaAsTopicSchemaThrowsException() { - PropertyResolver resolver = mock(PropertyResolver.class); - when(resolver.getListProperty("protobufFiles", String.class)) - .thenReturn(Optional.of(List.of(addressBookSchemaPath.toString(), sensorSchemaPath.toString()))); - when(resolver.getProperty("protobufMessageName", String.class)) - .thenReturn(Optional.of("test.AddressBook")); - - when(resolver.getMapProperty("protobufMessageNameByTopic", String.class, String.class)) - .thenReturn(Optional.of(Map.of("persons", "test.NotExistent"))); - - var serde = new ProtobufFileSerde(); - assertThatThrownBy(() -> serde.configure(resolver, resolver, resolver)) - .isInstanceOf(NullPointerException.class) - .hasMessage("The given message type not found in protobuf definition: test.NotExistent"); - } - - @Test - void unknownSchemaAsTopicSchemaForKeyThrowsException() { - PropertyResolver resolver = mock(PropertyResolver.class); - when(resolver.getListProperty("protobufFiles", String.class)) - .thenReturn(Optional.of(List.of(addressBookSchemaPath.toString(), sensorSchemaPath.toString()))); - when(resolver.getProperty("protobufMessageName", String.class)) - .thenReturn(Optional.of("test.AddressBook")); - - when(resolver.getMapProperty("protobufMessageNameForKeyByTopic", String.class, String.class)) - .thenReturn(Optional.of(Map.of("persons", "test.NotExistent"))); - - var serde = new ProtobufFileSerde(); - assertThatThrownBy(() -> serde.configure(resolver, resolver, resolver)) - .isInstanceOf(NullPointerException.class) - .hasMessage("The given message type not found in protobuf definition: test.NotExistent"); - } - @SneakyThrows private void assertJsonEquals(String expectedJson, String actualJson) { var mapper = new JsonMapper(); diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ConfigTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ConfigTest.java index 290a926ccf..6bc114b411 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ConfigTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ConfigTest.java @@ -4,10 +4,13 @@ import static org.assertj.core.api.Assertions.assertThat; import com.provectus.kafka.ui.AbstractIntegrationTest; import com.provectus.kafka.ui.model.BrokerConfigDTO; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.ServerStatusDTO; import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; @@ -19,6 +22,18 @@ public class ConfigTest extends AbstractIntegrationTest { @Autowired private WebTestClient webTestClient; + @BeforeEach + void waitUntilStatsInitialized() { + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInSameThread() + .until(() -> { + var stats = applicationContext.getBean(StatisticsCache.class) + .get(KafkaCluster.builder().name(LOCAL).build()); + return stats.getStatus() == ServerStatusDTO.ONLINE; + }); + } + @Test public void testAlterConfig() { String name = "background.threads"; diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KafkaConfigSanitizerTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KafkaConfigSanitizerTest.java index 0cc5a36c06..232e1d3703 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KafkaConfigSanitizerTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KafkaConfigSanitizerTest.java @@ -26,6 +26,13 @@ class KafkaConfigSanitizerTest { assertThat(sanitizer.sanitize("main.consumer.sasl.jaas.config", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("database.password", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("basic.auth.user.info", "secret")).isEqualTo("******"); + + //AWS var sanitizing + assertThat(sanitizer.sanitize("aws.access.key.id", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("aws.accessKeyId", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("aws.secret.access.key", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("aws.secretAccessKey", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("aws.sessionToken", "secret")).isEqualTo("******"); } @Test diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/MessagesServiceTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/MessagesServiceTest.java index ec2ae985f1..75a69adec7 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/MessagesServiceTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/MessagesServiceTest.java @@ -61,12 +61,12 @@ class MessagesServiceTest extends AbstractIntegrationTest { } @Test - void maskingAppliedOnConfiguredClusters() { + void maskingAppliedOnConfiguredClusters() throws Exception { String testTopic = MASKED_TOPICS_PREFIX + UUID.randomUUID(); try (var producer = KafkaTestProducer.forKafka(kafka)) { createTopic(new NewTopic(testTopic, 1, (short) 1)); producer.send(testTopic, "message1"); - producer.send(testTopic, "message2"); + producer.send(testTopic, "message2").get(); Flux msgsFlux = messagesService.loadMessages( cluster, @@ -91,4 +91,4 @@ class MessagesServiceTest extends AbstractIntegrationTest { } } -} \ No newline at end of file +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/OffsetsResetServiceTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/OffsetsResetServiceTest.java index 966e0ec763..fa73f0bff6 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/OffsetsResetServiceTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/OffsetsResetServiceTest.java @@ -37,24 +37,16 @@ public class OffsetsResetServiceTest extends AbstractIntegrationTest { private static final int PARTITIONS = 5; - private static final KafkaCluster CLUSTER = - KafkaCluster.builder() - .name(LOCAL) - .bootstrapServers(kafka.getBootstrapServers()) - .properties(new Properties()) - .build(); - private final String groupId = "OffsetsResetServiceTestGroup-" + UUID.randomUUID(); private final String topic = "OffsetsResetServiceTestTopic-" + UUID.randomUUID(); + private KafkaCluster cluster; private OffsetsResetService offsetsResetService; @BeforeEach void init() { - AdminClientServiceImpl adminClientService = new AdminClientServiceImpl(); - adminClientService.setClientTimeout(5_000); - offsetsResetService = new OffsetsResetService(adminClientService); - + cluster = applicationContext.getBean(ClustersStorage.class).getClusterByName(LOCAL).get(); + offsetsResetService = new OffsetsResetService(applicationContext.getBean(AdminClientService.class)); createTopic(new NewTopic(topic, PARTITIONS, (short) 1)); createConsumerGroup(); } @@ -76,13 +68,13 @@ public class OffsetsResetServiceTest extends AbstractIntegrationTest { void failsIfGroupDoesNotExists() { List> expectedNotFound = List.of( offsetsResetService - .resetToEarliest(CLUSTER, "non-existing-group", topic, null), + .resetToEarliest(cluster, "non-existing-group", topic, null), offsetsResetService - .resetToLatest(CLUSTER, "non-existing-group", topic, null), + .resetToLatest(cluster, "non-existing-group", topic, null), offsetsResetService - .resetToTimestamp(CLUSTER, "non-existing-group", topic, null, System.currentTimeMillis()), + .resetToTimestamp(cluster, "non-existing-group", topic, null, System.currentTimeMillis()), offsetsResetService - .resetToOffsets(CLUSTER, "non-existing-group", topic, Map.of()) + .resetToOffsets(cluster, "non-existing-group", topic, Map.of()) ); for (Mono mono : expectedNotFound) { @@ -101,11 +93,11 @@ public class OffsetsResetServiceTest extends AbstractIntegrationTest { consumer.poll(Duration.ofMillis(100)); List> expectedValidationError = List.of( - offsetsResetService.resetToEarliest(CLUSTER, groupId, topic, null), - offsetsResetService.resetToLatest(CLUSTER, groupId, topic, null), + offsetsResetService.resetToEarliest(cluster, groupId, topic, null), + offsetsResetService.resetToLatest(cluster, groupId, topic, null), offsetsResetService - .resetToTimestamp(CLUSTER, groupId, topic, null, System.currentTimeMillis()), - offsetsResetService.resetToOffsets(CLUSTER, groupId, topic, Map.of()) + .resetToTimestamp(cluster, groupId, topic, null, System.currentTimeMillis()), + offsetsResetService.resetToOffsets(cluster, groupId, topic, Map.of()) ); for (Mono mono : expectedValidationError) { @@ -121,7 +113,7 @@ public class OffsetsResetServiceTest extends AbstractIntegrationTest { sendMsgsToPartition(Map.of(0, 10, 1, 10, 2, 10)); var expectedOffsets = Map.of(0, 5L, 1, 5L, 2, 5L); - offsetsResetService.resetToOffsets(CLUSTER, groupId, topic, expectedOffsets).block(); + offsetsResetService.resetToOffsets(cluster, groupId, topic, expectedOffsets).block(); assertOffsets(expectedOffsets); } @@ -131,7 +123,7 @@ public class OffsetsResetServiceTest extends AbstractIntegrationTest { var offsetsWithInValidBounds = Map.of(0, -2L, 1, 5L, 2, 500L); var expectedOffsets = Map.of(0, 0L, 1, 5L, 2, 10L); - offsetsResetService.resetToOffsets(CLUSTER, groupId, topic, offsetsWithInValidBounds).block(); + offsetsResetService.resetToOffsets(cluster, groupId, topic, offsetsWithInValidBounds).block(); assertOffsets(expectedOffsets); } @@ -140,11 +132,11 @@ public class OffsetsResetServiceTest extends AbstractIntegrationTest { sendMsgsToPartition(Map.of(0, 10, 1, 10, 2, 10)); commit(Map.of(0, 5L, 1, 5L, 2, 5L)); - offsetsResetService.resetToEarliest(CLUSTER, groupId, topic, List.of(0, 1)).block(); + offsetsResetService.resetToEarliest(cluster, groupId, topic, List.of(0, 1)).block(); assertOffsets(Map.of(0, 0L, 1, 0L, 2, 5L)); commit(Map.of(0, 5L, 1, 5L, 2, 5L)); - offsetsResetService.resetToEarliest(CLUSTER, groupId, topic, null).block(); + offsetsResetService.resetToEarliest(cluster, groupId, topic, null).block(); assertOffsets(Map.of(0, 0L, 1, 0L, 2, 0L, 3, 0L, 4, 0L)); } @@ -153,11 +145,11 @@ public class OffsetsResetServiceTest extends AbstractIntegrationTest { sendMsgsToPartition(Map.of(0, 10, 1, 10, 2, 10, 3, 10, 4, 10)); commit(Map.of(0, 5L, 1, 5L, 2, 5L)); - offsetsResetService.resetToLatest(CLUSTER, groupId, topic, List.of(0, 1)).block(); + offsetsResetService.resetToLatest(cluster, groupId, topic, List.of(0, 1)).block(); assertOffsets(Map.of(0, 10L, 1, 10L, 2, 5L)); commit(Map.of(0, 5L, 1, 5L, 2, 5L)); - offsetsResetService.resetToLatest(CLUSTER, groupId, topic, null).block(); + offsetsResetService.resetToLatest(cluster, groupId, topic, null).block(); assertOffsets(Map.of(0, 10L, 1, 10L, 2, 10L, 3, 10L, 4, 10L)); } @@ -175,7 +167,7 @@ public class OffsetsResetServiceTest extends AbstractIntegrationTest { new ProducerRecord(topic, 2, 1200L, null, null))); offsetsResetService.resetToTimestamp( - CLUSTER, groupId, topic, List.of(0, 1, 2, 3), 1600L + cluster, groupId, topic, List.of(0, 1, 2, 3), 1600L ).block(); assertOffsets(Map.of(0, 2L, 1, 1L, 2, 3L, 3, 0L)); } @@ -227,7 +219,7 @@ public class OffsetsResetServiceTest extends AbstractIntegrationTest { private Consumer groupConsumer() { Properties props = new Properties(); props.put(ConsumerConfig.CLIENT_ID_CONFIG, "kafka-ui-" + UUID.randomUUID()); - props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, CLUSTER.getBootstrapServers()); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class); props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/ConnectorsExporterTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/ConnectorsExporterTest.java new file mode 100644 index 0000000000..20c0d96ad1 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/ConnectorsExporterTest.java @@ -0,0 +1,111 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.provectus.kafka.ui.connect.model.ConnectorTopics; +import com.provectus.kafka.ui.model.ConnectDTO; +import com.provectus.kafka.ui.model.ConnectorDTO; +import com.provectus.kafka.ui.model.ConnectorTypeDTO; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.service.KafkaConnectService; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.opendatadiscovery.client.model.DataEntity; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class ConnectorsExporterTest { + + private static final KafkaCluster CLUSTER = KafkaCluster.builder() + .name("test cluster") + .bootstrapServers("localhost:9092") + .build(); + + private final KafkaConnectService kafkaConnectService = mock(KafkaConnectService.class); + private final ConnectorsExporter exporter = new ConnectorsExporter(kafkaConnectService); + + @Test + void exportsConnectorsAsDataTransformers() { + ConnectDTO connect = new ConnectDTO(); + connect.setName("testConnect"); + connect.setAddress("http://kconnect:8083"); + + ConnectorDTO sinkConnector = new ConnectorDTO(); + sinkConnector.setName("testSink"); + sinkConnector.setType(ConnectorTypeDTO.SINK); + sinkConnector.setConnect(connect.getName()); + sinkConnector.setConfig( + Map.of( + "connector.class", "FileStreamSink", + "file", "filePathHere", + "topic", "inputTopic" + ) + ); + + ConnectorDTO sourceConnector = new ConnectorDTO(); + sourceConnector.setName("testSource"); + sourceConnector.setConnect(connect.getName()); + sourceConnector.setType(ConnectorTypeDTO.SOURCE); + sourceConnector.setConfig( + Map.of( + "connector.class", "FileStreamSource", + "file", "filePathHere", + "topic", "outputTopic" + ) + ); + + when(kafkaConnectService.getConnects(CLUSTER)) + .thenReturn(Flux.just(connect)); + + when(kafkaConnectService.getConnectorNames(CLUSTER, connect.getName())) + .thenReturn(Flux.just(sinkConnector.getName(), sourceConnector.getName())); + + when(kafkaConnectService.getConnector(CLUSTER, connect.getName(), sinkConnector.getName())) + .thenReturn(Mono.just(sinkConnector)); + + when(kafkaConnectService.getConnector(CLUSTER, connect.getName(), sourceConnector.getName())) + .thenReturn(Mono.just(sourceConnector)); + + when(kafkaConnectService.getConnectorTopics(CLUSTER, connect.getName(), sourceConnector.getName())) + .thenReturn(Mono.just(new ConnectorTopics().topics(List.of("outputTopic")))); + + when(kafkaConnectService.getConnectorTopics(CLUSTER, connect.getName(), sinkConnector.getName())) + .thenReturn(Mono.just(new ConnectorTopics().topics(List.of("inputTopic")))); + + StepVerifier.create(exporter.export(CLUSTER)) + .assertNext(dataEntityList -> { + assertThat(dataEntityList.getDataSourceOddrn()) + .isEqualTo("//kafkaconnect/host/kconnect:8083"); + + assertThat(dataEntityList.getItems()) + .hasSize(2); + + assertThat(dataEntityList.getItems()) + .filteredOn(DataEntity::getOddrn, "//kafkaconnect/host/kconnect:8083/connectors/testSink") + .singleElement() + .satisfies(sink -> { + assertThat(sink.getMetadata().get(0).getMetadata()) + .containsOnlyKeys("type", "connector.class", "file", "topic"); + assertThat(sink.getDataTransformer().getInputs()).contains( + "//kafka/cluster/localhost:9092/topics/inputTopic"); + }); + + assertThat(dataEntityList.getItems()) + .filteredOn(DataEntity::getOddrn, "//kafkaconnect/host/kconnect:8083/connectors/testSource") + .singleElement() + .satisfies(source -> { + assertThat(source.getMetadata().get(0).getMetadata()) + .containsOnlyKeys("type", "connector.class", "file", "topic"); + assertThat(source.getDataTransformer().getOutputs()).contains( + "//kafka/cluster/localhost:9092/topics/outputTopic"); + }); + + }) + .verifyComplete(); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporterTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporterTest.java new file mode 100644 index 0000000000..4d512612a6 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporterTest.java @@ -0,0 +1,167 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.Statistics; +import com.provectus.kafka.ui.service.StatisticsCache; +import com.provectus.kafka.ui.sr.api.KafkaSrClientApi; +import com.provectus.kafka.ui.sr.model.SchemaSubject; +import com.provectus.kafka.ui.sr.model.SchemaType; +import com.provectus.kafka.ui.util.ReactiveFailover; +import java.util.List; +import java.util.Map; +import org.apache.kafka.clients.admin.ConfigEntry; +import org.apache.kafka.clients.admin.TopicDescription; +import org.apache.kafka.common.Node; +import org.apache.kafka.common.TopicPartitionInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opendatadiscovery.client.model.DataEntity; +import org.opendatadiscovery.client.model.DataEntityType; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class TopicsExporterTest { + + private final KafkaSrClientApi schemaRegistryClientMock = mock(KafkaSrClientApi.class); + + private final KafkaCluster cluster = KafkaCluster.builder() + .name("testCluster") + .bootstrapServers("localhost:9092,localhost:19092") + .schemaRegistryClient(ReactiveFailover.createNoop(schemaRegistryClientMock)) + .build(); + + private Statistics stats; + + private TopicsExporter topicsExporter; + + @BeforeEach + void init() { + var statisticsCacheMock = mock(StatisticsCache.class); + when(statisticsCacheMock.get(cluster)).thenAnswer(invocationOnMock -> stats); + + topicsExporter = new TopicsExporter( + topic -> !topic.startsWith("_"), + statisticsCacheMock + ); + } + + @Test + void doesNotExportTopicsWhichDontFitFiltrationRule() { + when(schemaRegistryClientMock.getSubjectVersion(anyString(), anyString())) + .thenReturn(Mono.error(new RuntimeException("Not found"))); + + stats = Statistics.empty() + .toBuilder() + .topicDescriptions( + Map.of( + "_hidden", new TopicDescription("_hidden", false, List.of( + new TopicPartitionInfo(0, null, List.of(), List.of()) + )), + "visible", new TopicDescription("visible", false, List.of( + new TopicPartitionInfo(0, null, List.of(), List.of()) + )) + ) + ) + .build(); + + StepVerifier.create(topicsExporter.export(cluster)) + .assertNext(entityList -> { + assertThat(entityList.getDataSourceOddrn()) + .isNotEmpty(); + + assertThat(entityList.getItems()) + .hasSize(1) + .allSatisfy(e -> e.getOddrn().contains("visible")); + }) + .verifyComplete(); + } + + @Test + void doesExportTopicData() { + when(schemaRegistryClientMock.getSubjectVersion("testTopic-value", "latest")) + .thenReturn(Mono.just( + new SchemaSubject() + .schema("\"string\"") + .schemaType(SchemaType.AVRO) + )); + + when(schemaRegistryClientMock.getSubjectVersion("testTopic-key", "latest")) + .thenReturn(Mono.just( + new SchemaSubject() + .schema("\"int\"") + .schemaType(SchemaType.AVRO) + )); + + stats = Statistics.empty() + .toBuilder() + .topicDescriptions( + Map.of( + "testTopic", + new TopicDescription( + "testTopic", + false, + List.of( + new TopicPartitionInfo( + 0, + null, + List.of( + new Node(1, "host1", 9092), + new Node(2, "host2", 9092) + ), + List.of()) + )) + ) + ) + .topicConfigs( + Map.of( + "testTopic", List.of( + new ConfigEntry( + "custom.config", + "100500", + ConfigEntry.ConfigSource.DYNAMIC_TOPIC_CONFIG, + false, + false, + List.of(), + ConfigEntry.ConfigType.INT, + null + ) + ) + ) + ) + .build(); + + StepVerifier.create(topicsExporter.export(cluster)) + .assertNext(entityList -> { + assertThat(entityList.getItems()) + .hasSize(1); + + DataEntity topicEntity = entityList.getItems().get(0); + assertThat(topicEntity.getName()).isNotEmpty(); + assertThat(topicEntity.getOddrn()) + .isEqualTo("//kafka/cluster/localhost:19092,localhost:9092/topics/testTopic"); + assertThat(topicEntity.getType()).isEqualTo(DataEntityType.KAFKA_TOPIC); + assertThat(topicEntity.getMetadata()) + .hasSize(1) + .singleElement() + .satisfies(e -> + assertThat(e.getMetadata()) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + "partitions", 1, + "replication_factor", 2, + "custom.config", "100500"))); + + assertThat(topicEntity.getDataset()).isNotNull(); + assertThat(topicEntity.getDataset().getFieldList()) + .hasSize(4); // 2 field for key, 2 for value + }) + .verifyComplete(); + } + + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/AvroExtractorTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/AvroExtractorTest.java new file mode 100644 index 0000000000..d523d7cd41 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/AvroExtractorTest.java @@ -0,0 +1,272 @@ +package com.provectus.kafka.ui.service.integration.odd.schema; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.provectus.kafka.ui.sr.model.SchemaSubject; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.DataSetFieldType; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +class AvroExtractorTest { + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void test(boolean isKey) { + var list = AvroExtractor.extract( + new SchemaSubject() + .schema(""" + { + "type": "record", + "name": "Message", + "namespace": "com.provectus.kafka", + "fields": + [ + { + "name": "f1", + "type": + { + "type": "array", + "items": + { + "type": "record", + "name": "ArrElement", + "fields": + [ + { + "name": "longmap", + "type": + { + "type": "map", + "values": "long" + } + } + ] + } + } + }, + { + "name": "f2", + "type": + { + "type": "record", + "name": "InnerMessage", + "fields": + [ + { + "name": "text", + "doc": "string field here", + "type": "string" + }, + { + "name": "innerMsgRef", + "type": "InnerMessage" + }, + { + "name": "nullable_union", + "type": + [ + "null", + "string", + "int" + ], + "default": null + }, + { + "name": "order_enum", + "type": + { + "type": "enum", + "name": "Suit", + "symbols": + [ + "SPADES", + "HEARTS" + ] + } + }, + { + "name": "str_list", + "type": + { + "type": "array", + "items": "string" + } + } + ] + } + } + ] + } + """), + + KafkaPath.builder() + .cluster("localhost:9092") + .topic("someTopic") + .build(), + isKey + ); + + String baseOddrn = "//kafka/cluster/localhost:9092/topics/someTopic/columns/" + (isKey ? "key" : "value"); + + assertThat(list).contains( + DataSetFieldsExtractors.rootField( + KafkaPath.builder().cluster("localhost:9092").topic("someTopic").build(), + isKey + ), + new DataSetField() + .name("f1") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/f1") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.LIST) + .logicalType("array") + .isNullable(false) + ), + new DataSetField() + .name("ArrElement") + .parentFieldOddrn(baseOddrn + "/f1") + .oddrn(baseOddrn + "/f1/items/ArrElement") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("com.provectus.kafka.ArrElement") + .isNullable(false) + ), + new DataSetField() + .name("longmap") + .parentFieldOddrn(baseOddrn + "/f1/items/ArrElement") + .oddrn(baseOddrn + "/f1/items/ArrElement/fields/longmap") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.MAP) + .logicalType("map") + .isNullable(false) + ), + new DataSetField() + .name("key") + .parentFieldOddrn(baseOddrn + "/f1/items/ArrElement/fields/longmap") + .oddrn(baseOddrn + "/f1/items/ArrElement/fields/longmap/key") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRING) + .logicalType("string") + .isNullable(false) + ), + new DataSetField() + .name("value") + .parentFieldOddrn(baseOddrn + "/f1/items/ArrElement/fields/longmap") + .oddrn(baseOddrn + "/f1/items/ArrElement/fields/longmap/value") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.INTEGER) + .logicalType("long") + .isNullable(false) + ), + new DataSetField() + .name("f2") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/f2") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("com.provectus.kafka.InnerMessage") + .isNullable(false) + ), + new DataSetField() + .name("text") + .parentFieldOddrn(baseOddrn + "/f2") + .oddrn(baseOddrn + "/f2/fields/text") + .description("string field here") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRING) + .logicalType("string") + .isNullable(false) + ), + new DataSetField() + .name("innerMsgRef") + .parentFieldOddrn(baseOddrn + "/f2") + .oddrn(baseOddrn + "/f2/fields/innerMsgRef") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("com.provectus.kafka.InnerMessage") + .isNullable(false) + ), + new DataSetField() + .name("nullable_union") + .parentFieldOddrn(baseOddrn + "/f2") + .oddrn(baseOddrn + "/f2/fields/nullable_union") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.UNION) + .logicalType("union") + .isNullable(true) + ), + new DataSetField() + .name("string") + .parentFieldOddrn(baseOddrn + "/f2/fields/nullable_union") + .oddrn(baseOddrn + "/f2/fields/nullable_union/values/string") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRING) + .logicalType("string") + .isNullable(true) + ), + new DataSetField() + .name("int") + .parentFieldOddrn(baseOddrn + "/f2/fields/nullable_union") + .oddrn(baseOddrn + "/f2/fields/nullable_union/values/int") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.INTEGER) + .logicalType("int") + .isNullable(true) + ), + new DataSetField() + .name("int") + .parentFieldOddrn(baseOddrn + "/f2/fields/nullable_union") + .oddrn(baseOddrn + "/f2/fields/nullable_union/values/int") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.INTEGER) + .logicalType("int") + .isNullable(true) + ), + new DataSetField() + .name("order_enum") + .parentFieldOddrn(baseOddrn + "/f2") + .oddrn(baseOddrn + "/f2/fields/order_enum") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRING) + .logicalType("enum") + .isNullable(false) + ), + new DataSetField() + .name("str_list") + .parentFieldOddrn(baseOddrn + "/f2") + .oddrn(baseOddrn + "/f2/fields/str_list") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.LIST) + .logicalType("array") + .isNullable(false) + ), + new DataSetField() + .name("string") + .parentFieldOddrn(baseOddrn + "/f2/fields/str_list") + .oddrn(baseOddrn + "/f2/fields/str_list/items/string") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRING) + .logicalType("string") + .isNullable(false) + ) + ); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/JsonSchemaExtractorTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/JsonSchemaExtractorTest.java new file mode 100644 index 0000000000..7968e52e6d --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/JsonSchemaExtractorTest.java @@ -0,0 +1,145 @@ +package com.provectus.kafka.ui.service.integration.odd.schema; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.provectus.kafka.ui.sr.model.SchemaSubject; +import java.net.URI; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.DataSetFieldType; +import org.opendatadiscovery.client.model.MetadataExtension; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +class JsonSchemaExtractorTest { + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void test(boolean isKey) { + String jsonSchema = """ + { + "$id": "http://example.com/test.TestMsg", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": [ "int32_field" ], + "properties": + { + "int32_field": { "type": "integer", "title": "field title" }, + "lst_s_field": { "type": "array", "items": { "type": "string" }, "description": "field descr" }, + "untyped_struct_field": { "type": "object", "properties": {} }, + "union_field": { "type": [ "number", "object", "null" ] }, + "struct_field": { + "type": "object", + "properties": { + "bool_field": { "type": "boolean" } + } + } + } + } + """; + var fields = JsonSchemaExtractor.extract( + new SchemaSubject().schema(jsonSchema), + KafkaPath.builder() + .cluster("localhost:9092") + .topic("someTopic") + .build(), + isKey + ); + + String baseOddrn = "//kafka/cluster/localhost:9092/topics/someTopic/columns/" + (isKey ? "key" : "value"); + + assertThat(fields).contains( + DataSetFieldsExtractors.rootField( + KafkaPath.builder().cluster("localhost:9092").topic("someTopic").build(), + isKey + ), + new DataSetField() + .name("int32_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/int32_field") + .description("field title") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.NUMBER) + .logicalType("Number") + .isNullable(false)), + new DataSetField() + .name("lst_s_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/lst_s_field") + .description("field descr") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.LIST) + .logicalType("array") + .isNullable(true)), + new DataSetField() + .name("String") + .parentFieldOddrn(baseOddrn + "/lst_s_field") + .oddrn(baseOddrn + "/lst_s_field/items/String") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRING) + .logicalType("String") + .isNullable(false)), + new DataSetField() + .name("untyped_struct_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/untyped_struct_field") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("Object") + .isNullable(true)), + new DataSetField() + .name("union_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/union_field/anyOf") + .metadata(List.of(new MetadataExtension() + .schemaUrl(URI.create("wontbeused.oops")) + .metadata(Map.of("criterion", "anyOf")))) + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.UNION) + .logicalType("anyOf") + .isNullable(true)), + new DataSetField() + .name("Number") + .parentFieldOddrn(baseOddrn + "/union_field/anyOf") + .oddrn(baseOddrn + "/union_field/anyOf/values/Number") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.NUMBER) + .logicalType("Number") + .isNullable(true)), + new DataSetField() + .name("Object") + .parentFieldOddrn(baseOddrn + "/union_field/anyOf") + .oddrn(baseOddrn + "/union_field/anyOf/values/Object") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("Object") + .isNullable(true)), + new DataSetField() + .name("Null") + .parentFieldOddrn(baseOddrn + "/union_field/anyOf") + .oddrn(baseOddrn + "/union_field/anyOf/values/Null") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.UNKNOWN) + .logicalType("Null") + .isNullable(true)), + new DataSetField() + .name("struct_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/struct_field") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("Object") + .isNullable(true)), + new DataSetField() + .name("bool_field") + .parentFieldOddrn(baseOddrn + "/struct_field") + .oddrn(baseOddrn + "/struct_field/fields/bool_field") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.BOOLEAN) + .logicalType("Boolean") + .isNullable(true)) + ); + } +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/ProtoExtractorTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/ProtoExtractorTest.java new file mode 100644 index 0000000000..cbb97a859c --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/ProtoExtractorTest.java @@ -0,0 +1,187 @@ +package com.provectus.kafka.ui.service.integration.odd.schema; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.provectus.kafka.ui.sr.model.SchemaSubject; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.DataSetFieldType; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +class ProtoExtractorTest { + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void test(boolean isKey) { + String protoSchema = """ + syntax = "proto3"; + package test; + + import "google/protobuf/timestamp.proto"; + import "google/protobuf/duration.proto"; + import "google/protobuf/struct.proto"; + import "google/protobuf/wrappers.proto"; + + message TestMsg { + map mapField = 100; + int32 int32_field = 2; + bool bool_field = 3; + SampleEnum enum_field = 4; + + enum SampleEnum { + ENUM_V1 = 0; + ENUM_V2 = 1; + } + + google.protobuf.Timestamp ts_field = 5; + google.protobuf.Duration duration_field = 8; + + oneof some_oneof1 { + google.protobuf.Value one_of_v1 = 9; + google.protobuf.Value one_of_v2 = 10; + } + // wrapper field: + google.protobuf.Int64Value int64_w_field = 11; + + //embedded msg + EmbeddedMsg emb = 19; + + message EmbeddedMsg { + int32 emb_f1 = 1; + TestMsg outer_ref = 2; + } + }"""; + + var list = ProtoExtractor.extract( + new SchemaSubject() + .schema(protoSchema), + KafkaPath.builder() + .cluster("localhost:9092") + .topic("someTopic") + .build(), + isKey + ); + + String baseOddrn = "//kafka/cluster/localhost:9092/topics/someTopic/columns/" + (isKey ? "key" : "value"); + + assertThat(list) + .contains( + DataSetFieldsExtractors.rootField( + KafkaPath.builder().cluster("localhost:9092").topic("someTopic").build(), + isKey + ), + new DataSetField() + .name("mapField") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/mapField") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.LIST) + .logicalType("repeated") + .isNullable(true) + ), + new DataSetField() + .name("int32_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/int32_field") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.INTEGER) + .logicalType("int32") + .isNullable(true) + ), + new DataSetField() + .name("enum_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/enum_field") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRING) + .logicalType("enum") + .isNullable(true) + ), + new DataSetField() + .name("ts_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/ts_field") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.DATETIME) + .logicalType("google.protobuf.Timestamp") + .isNullable(true) + ), + new DataSetField() + .name("duration_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/duration_field") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.DURATION) + .logicalType("google.protobuf.Duration") + .isNullable(true) + ), + new DataSetField() + .name("one_of_v1") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/one_of_v1") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.UNKNOWN) + .logicalType("google.protobuf.Value") + .isNullable(true) + ), + new DataSetField() + .name("one_of_v2") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/one_of_v2") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.UNKNOWN) + .logicalType("google.protobuf.Value") + .isNullable(true) + ), + new DataSetField() + .name("int64_w_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/int64_w_field") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.INTEGER) + .logicalType("google.protobuf.Int64Value") + .isNullable(true) + ), + new DataSetField() + .name("emb") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/emb") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("test.TestMsg.EmbeddedMsg") + .isNullable(true) + ), + new DataSetField() + .name("emb_f1") + .parentFieldOddrn(baseOddrn + "/emb") + .oddrn(baseOddrn + "/emb/fields/emb_f1") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.INTEGER) + .logicalType("int32") + .isNullable(true) + ), + new DataSetField() + .name("outer_ref") + .parentFieldOddrn(baseOddrn + "/emb") + .oddrn(baseOddrn + "/emb/fields/outer_ref") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("test.TestMsg") + .isNullable(true) + ) + ); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlApiClientTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlApiClientTest.java index 069b173100..cde000ac6e 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlApiClientTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlApiClientTest.java @@ -125,7 +125,7 @@ class KsqlApiClientTest extends AbstractIntegrationTest { } private KsqlApiClient ksqlClient() { - return new KsqlApiClient(KSQL_DB.url(), null, null, null); + return new KsqlApiClient(KSQL_DB.url(), null, null, null, null); } diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2Test.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2Test.java index a4098b0275..b4a48d3879 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2Test.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2Test.java @@ -114,7 +114,7 @@ class KsqlServiceV2Test extends AbstractIntegrationTest { } private static KsqlApiClient ksqlClient() { - return new KsqlApiClient(KSQL_DB.url(), null, null, null); + return new KsqlApiClient(KSQL_DB.url(), null, null, null, null); } } diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/response/ResponseParserTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/response/ResponseParserTest.java new file mode 100644 index 0000000000..0255244933 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/response/ResponseParserTest.java @@ -0,0 +1,25 @@ +package com.provectus.kafka.ui.service.ksql.response; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class ResponseParserTest { + + @Test + void parsesSelectHeaderIntoColumnNames() { + assertThat(ResponseParser.parseSelectHeadersString("`inQuotes` INT, notInQuotes INT")) + .containsExactly("`inQuotes` INT", "notInQuotes INT"); + + assertThat(ResponseParser.parseSelectHeadersString("`name with comma,` INT, name2 STRING")) + .containsExactly("`name with comma,` INT", "name2 STRING"); + + assertThat(ResponseParser.parseSelectHeadersString( + "`topLvl` INT, `struct` STRUCT<`nested1` STRING, anotherName STRUCT>")) + .containsExactly( + "`topLvl` INT", + "`struct` STRUCT<`nested1` STRING, anotherName STRUCT>" + ); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetrieverTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetrieverTest.java index f4e8cdfe75..9cc0494039 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetrieverTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetrieverTest.java @@ -15,7 +15,7 @@ import reactor.test.StepVerifier; class PrometheusMetricsRetrieverTest { - private final PrometheusMetricsRetriever retriever = new PrometheusMetricsRetriever(WebClient.create()); + private final PrometheusMetricsRetriever retriever = new PrometheusMetricsRetriever(); private final MockWebServer mockWebServer = new MockWebServer(); @@ -36,7 +36,7 @@ class PrometheusMetricsRetrieverTest { MetricsConfig metricsConfig = prepareMetricsConfig(url.port(), null, null); - StepVerifier.create(retriever.retrieve(url.host(), metricsConfig)) + StepVerifier.create(retriever.retrieve(WebClient.create(), url.host(), metricsConfig)) .expectNextSequence(expectedRawMetrics()) // third metric should not be present, since it has "NaN" value .verifyComplete(); @@ -50,7 +50,7 @@ class PrometheusMetricsRetrieverTest { MetricsConfig metricsConfig = prepareMetricsConfig(url.port(), "username", "password"); - StepVerifier.create(retriever.retrieve(url.host(), metricsConfig)) + StepVerifier.create(retriever.retrieve(WebClient.create(), url.host(), metricsConfig)) .expectNextSequence(expectedRawMetrics()) // third metric should not be present, since it has "NaN" value .verifyComplete(); diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/DynamicConfigOperationsTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/DynamicConfigOperationsTest.java new file mode 100644 index 0000000000..7355a9666f --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/DynamicConfigOperationsTest.java @@ -0,0 +1,128 @@ +package com.provectus.kafka.ui.util; + +import static com.provectus.kafka.ui.util.DynamicConfigOperations.DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY; +import static com.provectus.kafka.ui.util.DynamicConfigOperations.DYNAMIC_CONFIG_PATH_ENV_PROPERTY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.provectus.kafka.ui.config.ClustersProperties; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; + +class DynamicConfigOperationsTest { + + private static final String SAMPLE_YAML_CONFIG = """ + kafka: + clusters: + - name: test + bootstrapServers: localhost:9092 + """; + + private final ConfigurableApplicationContext ctxMock = mock(ConfigurableApplicationContext.class); + private final ConfigurableEnvironment envMock = mock(ConfigurableEnvironment.class); + + private final DynamicConfigOperations ops = new DynamicConfigOperations(ctxMock); + + @TempDir + private Path tmpDir; + + @BeforeEach + void initMocks() { + when(ctxMock.getEnvironment()).thenReturn(envMock); + } + + @Test + void initializerAddsDynamicPropertySourceIfAllEnvVarsAreSet() throws Exception { + Path propsFilePath = tmpDir.resolve("props.yaml"); + Files.writeString(propsFilePath, SAMPLE_YAML_CONFIG, StandardOpenOption.CREATE); + + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addFirst(new MapPropertySource("test", Map.of("testK", "testV"))); + + when(envMock.getPropertySources()).thenReturn(propertySources); + mockEnvWithVars(Map.of( + DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY, "true", + DYNAMIC_CONFIG_PATH_ENV_PROPERTY, propsFilePath.toString() + )); + + DynamicConfigOperations.dynamicConfigPropertiesInitializer().initialize(ctxMock); + + assertThat(propertySources.size()).isEqualTo(2); + assertThat(propertySources.stream()) + .element(0) + .extracting(PropertySource::getName) + .isEqualTo("dynamicProperties"); + } + + @ParameterizedTest + @CsvSource({ + "false, /tmp/conf.yaml", + "true, ", + ", /tmp/conf.yaml", + ",", + "true, /tmp/conf.yaml", //vars set, but file doesn't exist + }) + void initializerDoNothingIfAnyOfEnvVarsNotSet(@Nullable String enabledVar, @Nullable String pathVar) { + var vars = new HashMap(); // using HashMap to keep null values + vars.put(DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY, enabledVar); + vars.put(DYNAMIC_CONFIG_PATH_ENV_PROPERTY, pathVar); + mockEnvWithVars(vars); + + DynamicConfigOperations.dynamicConfigPropertiesInitializer().initialize(ctxMock); + verify(envMock, times(0)).getPropertySources(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void persistRewritesOrCreateConfigFile(boolean exists) throws Exception { + Path propsFilePath = tmpDir.resolve("props.yaml"); + if (exists) { + Files.writeString(propsFilePath, SAMPLE_YAML_CONFIG, StandardOpenOption.CREATE); + } + + mockEnvWithVars(Map.of( + DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY, "true", + DYNAMIC_CONFIG_PATH_ENV_PROPERTY, propsFilePath.toString() + )); + + var overrideProps = new ClustersProperties(); + var cluster = new ClustersProperties.Cluster(); + cluster.setName("newName"); + overrideProps.setClusters(List.of(cluster)); + + ops.persist( + DynamicConfigOperations.PropertiesStructure.builder() + .kafka(overrideProps) + .build() + ); + + assertThat(ops.loadDynamicPropertySource()) + .get() + .extracting(ps -> ps.getProperty("kafka.clusters[0].name")) + .isEqualTo("newName"); + } + + private void mockEnvWithVars(Map envVars) { + envVars.forEach((k, v) -> when(envMock.getProperty(k)).thenReturn((String) v)); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverterTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverterTest.java index 662c3ae22b..02da33bb12 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverterTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverterTest.java @@ -57,6 +57,7 @@ class ProtobufSchemaConverterTest { message EmbeddedMsg { int32 emb_f1 = 1; TestMsg outer_ref = 2; + EmbeddedMsg self_ref = 3; } }"""; @@ -116,7 +117,8 @@ class ProtobufSchemaConverterTest { "properties": { "emb_f1": { "type": "integer", "maximum": 2147483647, "minimum": -2147483648 }, - "outer_ref": { "$ref": "#/definitions/test.TestMsg" } + "outer_ref": { "$ref": "#/definitions/test.TestMsg" }, + "self_ref": { "$ref": "#/definitions/test.TestMsg.EmbeddedMsg" } } } }, diff --git a/kafka-ui-api/src/test/resources/address-book.proto b/kafka-ui-api/src/test/resources/protobuf-serde/address-book.proto similarity index 81% rename from kafka-ui-api/src/test/resources/address-book.proto rename to kafka-ui-api/src/test/resources/protobuf-serde/address-book.proto index 72eab7aab8..f6c9a5d788 100644 --- a/kafka-ui-api/src/test/resources/address-book.proto +++ b/kafka-ui-api/src/test/resources/protobuf-serde/address-book.proto @@ -1,16 +1,10 @@ -// [START declaration] syntax = "proto3"; package test; -// [END declaration] - -// [START java_declaration] option java_multiple_files = true; option java_package = "com.example.tutorial.protos"; option java_outer_classname = "AddressBookProtos"; -// [END java_declaration] -// [START messages] message Person { string name = 1; int32 id = 2; // Unique ID number for this person. @@ -31,9 +25,13 @@ message Person { } +message AnotherPerson { + string name = 1; + string surname = 2; +} + // Our address book file is just one of these. message AddressBook { int32 version = 1; repeated Person people = 2; } -// [END messages] \ No newline at end of file diff --git a/kafka-ui-api/src/test/resources/protobuf-serde/lang-description.proto b/kafka-ui-api/src/test/resources/protobuf-serde/lang-description.proto new file mode 100644 index 0000000000..8e213d58c4 --- /dev/null +++ b/kafka-ui-api/src/test/resources/protobuf-serde/lang-description.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package test; + +import "language/language.proto"; +import "google/protobuf/wrappers.proto"; + +message LanguageDescription { + test.lang.Language lang = 1; + google.protobuf.StringValue descr = 2; +} diff --git a/kafka-ui-api/src/test/resources/protobuf-serde/language/language.proto b/kafka-ui-api/src/test/resources/protobuf-serde/language/language.proto new file mode 100644 index 0000000000..7ef30eab23 --- /dev/null +++ b/kafka-ui-api/src/test/resources/protobuf-serde/language/language.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; +package test.lang; + +enum Language { + DE = 0; + EN = 1; + ES = 2; + FR = 3; + PL = 4; + RU = 5; +} diff --git a/kafka-ui-api/src/test/resources/sensor.proto b/kafka-ui-api/src/test/resources/protobuf-serde/sensor.proto similarity index 93% rename from kafka-ui-api/src/test/resources/sensor.proto rename to kafka-ui-api/src/test/resources/protobuf-serde/sensor.proto index 33b8c387e4..3bde20a3ae 100644 --- a/kafka-ui-api/src/test/resources/sensor.proto +++ b/kafka-ui-api/src/test/resources/protobuf-serde/sensor.proto @@ -1,5 +1,5 @@ syntax = "proto3"; -package iot; +package test; message Sensor { string name = 1; diff --git a/kafka-ui-contract/pom.xml b/kafka-ui-contract/pom.xml index 3e8437258b..5f6cb58b7d 100644 --- a/kafka-ui-contract/pom.xml +++ b/kafka-ui-contract/pom.xml @@ -99,6 +99,9 @@ java8 + + filepart=org.springframework.http.codec.multipart.FilePart + diff --git a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml index 25fdd25da8..1f994b0aff 100644 --- a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml +++ b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml @@ -1848,6 +1848,90 @@ paths: schema: $ref: '#/components/schemas/AuthenticationInfo' + /api/info: + get: + tags: + - ApplicationConfig + summary: Gets application info + operationId: getApplicationInfo + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationInfo' + + /api/config: + get: + tags: + - ApplicationConfig + summary: Gets current application configuration + operationId: getCurrentConfig + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationConfig' + put: + tags: + - ApplicationConfig + summary: Restarts application with specified configuration + operationId: restartWithConfig + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RestartRequest' + responses: + 200: + description: OK + + /api/config/validated: + put: + tags: + - ApplicationConfig + summary: Restarts application with specified configuration + operationId: validateConfig + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationConfig' + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationConfigValidation' + + + /api/config/relatedfiles: + post: + tags: + - ApplicationConfig + summary: Restarts application with specified configuration + operationId: uploadConfigRelatedFile + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: filepart + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UploadedFileInfo' + components: schemas: TopicSerdeSuggestion: @@ -1928,6 +2012,16 @@ components: stackTrace: type: string + ApplicationInfo: + type: object + properties: + enabledFeatures: + type: array + items: + type: string + enum: + - DYNAMIC_CONFIG + Cluster: type: object properties: @@ -3310,6 +3404,7 @@ components: ResourceType: type: string enum: + - APPLICATIONCONFIG - CLUSTERCONFIG - TOPIC - CONSUMER @@ -3360,3 +3455,283 @@ components: enum: - ALLOW - DENY + + RestartRequest: + type: object + properties: + config: + $ref: '#/components/schemas/ApplicationConfig' + + UploadedFileInfo: + type: object + required: [location] + properties: + location: + type: string + + ApplicationConfigValidation: + type: object + properties: + clusters: + type: object + additionalProperties: + $ref: '#/components/schemas/ClusterConfigValidation' + + ApplicationPropertyValidation: + type: object + required: [error] + properties: + error: + type: boolean + errorMessage: + type: string + description: Contains error message if error = true + + ClusterConfigValidation: + type: object + required: [kafka] + properties: + kafka: + $ref: '#/components/schemas/ApplicationPropertyValidation' + schemaRegistry: + $ref: '#/components/schemas/ApplicationPropertyValidation' + kafkaConnects: + type: object + additionalProperties: + $ref: '#/components/schemas/ApplicationPropertyValidation' + ksqldb: + $ref: '#/components/schemas/ApplicationPropertyValidation' + + ApplicationConfig: + type: object + properties: + properties: + type: object + properties: + auth: + type: object + properties: + type: + type: string + oauth2: + type: object + properties: + client: + type: object + additionalProperties: + type: object + properties: + provider: + type: string + clientId: + type: string + clientSecret: + type: string + clientName: + type: string + redirectUri: + type: string + authorizationGrantType: + type: string + issuerUri: + type: string + authorizationUri: + type: string + tokenUri: + type: string + userInfoUri: + type: string + jwkSetUri: + type: string + userNameAttribute: + type: string + scope: + type: array + items: + type: string + customParams: + type: object + additionalProperties: + type: string + rbac: + type: object + properties: + roles: + type: array + items: + type: object + properties: + name: + type: string + clusters: + type: array + items: + type: string + subjects: + type: array + items: + type: object + properties: + provider: + type: string + type: + type: string + value: + type: string + permissions: + type: array + items: + type: object + properties: + resource: + $ref: '#/components/schemas/ResourceType' + value: + type: string + actions: + type: array + items: + $ref: '#/components/schemas/Action' + kafka: + type: object + properties: + clusters: + type: array + items: + type: object + properties: + name: + type: string + bootstrapServers: + type: string + ssl: + type: object + properties: + truststoreLocation: + type: string + truststorePassword: + type: string + schemaRegistry: + type: string + schemaRegistryAuth: + type: object + properties: + username: + type: string + password: + type: string + schemaRegistrySsl: + type: object + properties: + keystoreLocation: + type: string + keystorePassword: + type: string + ksqldbServer: + type: string + ksqldbServerSsl: + type: object + properties: + keystoreLocation: + type: string + keystorePassword: + type: string + ksqldbServerAuth: + type: object + properties: + username: + type: string + password: + type: string + kafkaConnect: + type: array + items: + type: object + properties: + name: + type: string + address: + type: string + username: + type: string + password: + type: string + keystoreLocation: + type: string + keystorePassword: + type: string + + metrics: + type: object + properties: + type: + type: string + port: + type: integer + format: int32 + ssl: + type: boolean + username: + type: string + password: + type: string + keystoreLocation: + type: string + keystorePassword: + type: string + properties: + type: object + additionalProperties: true + readOnly: + type: boolean + disableLogDirsCollection: + type: boolean + serde: + type: array + items: + type: object + properties: + name: + type: string + className: + type: string + filePath: + type: string + properties: + type: object + additionalProperties: true + topicKeysPattern: + type: string + topicValuesPattern: + type: string + defaultKeySerde: + type: string + defaultValueSerde: + type: string + masking: + type: array + items: + type: object + properties: + type: + type: string + enum: + - REMOVE + - MASK + - REPLACE + fields: + type: array + items: + type: string + pattern: + type: array + items: + type: string + replacement: + type: string + topicKeysPattern: + type: string + topicValuesPattern: + type: string + pollingThrottleRate: + type: integer + format: int64 diff --git a/kafka-ui-e2e-checks/QASE.md b/kafka-ui-e2e-checks/QASE.md new file mode 100644 index 0000000000..b09731515a --- /dev/null +++ b/kafka-ui-e2e-checks/QASE.md @@ -0,0 +1,70 @@ +### E2E integration with Qase.io TMS (for internal users) + +### Table of Contents + +- [Intro](#intro) +- [Set up Qase.io integration](#set-up-qase-integration) +- [Test case creation](#test-case-creation) +- [Test run reporting](#test-run-reporting) + +### Intro + +We're using [Qase.io](https://help.qase.io/en/) as TMS to keep test cases and accumulate test runs. +Integration is set up through API using [qase-api](https://mvnrepository.com/artifact/io.qase/qase-api) +and [qase-testng](https://mvnrepository.com/artifact/io.qase/qase-testng) libraries. + +### Set up Qase integration + +To set up integration locally add next VM option `-DQASEIO_API_TOKEN='%s'` +(add your [Qase token](https://app.qase.io/user/api/token) instead of '%s') into your run configuration + +### Test case creation + +All new test cases can be added into TMS by default if they have no QaseId and QaseTitle matching already existing +cases. +But to handle `@Suite` and `@Automation` we added custom QaseCreateListener. To create new test case for next sync with +Qase (see example `kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/Template.java`): + +1. Create new class in `kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/suit` +2. Inherit it from `kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/BaseQaseTest.java` +3. Create new test method with some name inside the class and annotate it with: + +- `@Automation` (optional - Not automated by default) - to set one of automation states: NOT_AUTOMATED, TO_BE_AUTOMATED, + AUTOMATED +- `@QaseTitle` (required) - to set title for new test case and to check is there no existing cases with same title in + Qase.io +- `@Status` (optional - Draft by default) - to set one of case statuses: ACTUAL, DRAFT, DEPRECATED +- `@Suite` (optional) - to store new case in some existing package need to set its id, otherwise case will be stored in + the root +- `@Test` (required) - annotation from TestNG to specify this method as test + +4. Create new private void step methods with some name inside the same class and annotate it with + @io.qase.api.annotation.Step to specify this method as step. +5. Use defined step methods inside created test method in concrete order +6. If there are any additional cases to create you can repeat scenario in a new class +7. There are two ways to sync newly created cases in the framework with Qase.io: + +- sync can be performed locally - run new test classes with + already [set up Qase.io integration](#Set up Qase.io integration) +- also you can commit and push your changes, then + run [E2E Manual suite](https://github.com/provectus/kafka-ui/actions/workflows/e2e-manual.yml) on your branch + +8. No test run in Qase.io will be created, new test case will be stored defined directory + in [project's repository](https://app.qase.io/project/KAFKAUI) +9. To add expected results into created test case edit in Qase.io manually + +### Test run reporting + +To handle manual test cases with status `Skipped` we added custom QaseResultListener. To create new test run: + +1. All test methods should be annotated with actual `@QaseId` +2. There are two ways to sync newly created cases in the framework with Qase.io: + +- run can be performed locally - run test classes (or suites) with + already [set up Qase.io integration](#Set up Qase.io integration), they will be labeled as `Automation CUSTOM suite` +- also you can commit and push your changes, then + run [E2E Automation suite](https://github.com/provectus/kafka-ui/actions/workflows/e2e-automation.yml) on your branch + +3. All new test runs will be added into [project's test runs](https://app.qase.io/run/KAFKAUI) with corresponding label + using QaseId to identify existing cases +4. All test cases from manual suite are set up to have `Skipped` status in test runs to perform them manually diff --git a/kafka-ui-e2e-checks/README.md b/kafka-ui-e2e-checks/README.md index 42f17fc84a..d7f3c77c1f 100644 --- a/kafka-ui-e2e-checks/README.md +++ b/kafka-ui-e2e-checks/README.md @@ -1,12 +1,13 @@ ### E2E UI automation for Kafka-ui -This repository is for E2E UI automation. +This repository is for E2E UI automation. ### Table of Contents - [Prerequisites](#prerequisites) - [How to install](#how-to-install) - [How to run checks](#how-to-run-checks) +- [Qase.io integration (for internal users)](#qase-integration) - [Reporting](#reporting) - [Environments setup](#environments-setup) - [Test Data](#test-data) @@ -16,56 +17,83 @@ This repository is for E2E UI automation. - [How to develop](#how-to-develop) ### Prerequisites + - Docker & Docker-compose - Java (install aarch64 jdk if you have M1/arm chip) - Maven - + ### How to install + ``` git clone https://github.com/provectus/kafka-ui.git cd kafka-ui-e2e-checks docker pull selenoid/vnc:chrome_86.0 ``` + ### How to run checks -1. Run `kafka-ui`: +1. Run `kafka-ui`: + ``` cd kafka-ui docker-compose -f documentation/compose/e2e-tests.yaml up -d ``` -2. Run tests using your QaseIO API token as environment variable (put instead %s into command below) + +2. To run test suite select its name (options: regression, sanity, smoke) and put it instead %s into command below + ``` -./mvnw -DQASEIO_API_TOKEN='%s' -pl '!kafka-ui-api' test -Pprod +./mvnw -Dsurefire.suiteXmlFiles='src/test/resources/%s.xml' -f 'kafka-ui-e2e-checks' test -Pprod ``` + 3. To run tests on your local Chrome browser just add next VM option to the Run Configuration + ``` -Dbrowser=local ``` +### Qase integration + +Found instruction for Qase.io integration (for internal use only) at `kafka-ui-e2e-checks/QASE.md` + ### Reporting Reports are in `allure-results` folder. If you have installed allure commandline [here](https://www.npmjs.com/package/allure-commandline)) You can see allure report with command: + ``` allure serve ``` + ### Screenshots Reference screenshots are in `SCREENSHOTS_FOLDER` (default,`kafka-ui-e2e-checks/screenshots`) ### How to develop -> ⚠️ todo -### Setting for different environments -> ⚠️ todo -### Test Data -> ⚠️ todo -### Actions -> ⚠️ todo -### Checks -> ⚠️ todo -### Parallelization -> ⚠️ todo -### Tips - - install `Selenium UI Testing plugin` in IDEA + +> ⚠️ todo + +### Setting for different environments + +> ⚠️ todo + +### Test Data + +> ⚠️ todo + +### Actions + +> ⚠️ todo + +### Checks + +> ⚠️ todo + +### Parallelization + +> ⚠️ todo + +### Tips + +- install `Selenium UI Testing plugin` in IDEA diff --git a/kafka-ui-e2e-checks/pom.xml b/kafka-ui-e2e-checks/pom.xml index 1d1aaf3d5f..0b458cf173 100644 --- a/kafka-ui-e2e-checks/pom.xml +++ b/kafka-ui-e2e-checks/pom.xml @@ -1,42 +1,35 @@ - kafka-ui com.provectus 0.0.1-SNAPSHOT - 4.0.0 + 4.0.0 kafka-ui-e2e-checks + + 3.0.0-M8 ${project.version} + 1.17.6 + 5.2.1 + 4.8.1 + 6.11.2 + 7.7.0 + 2.21.0 + 3.0.3 1.9.9.1 - 2.18.1 - 2.4.8 - 6.6.3 - 3.23.1 - 1.0.1 + 3.24.2 2.2 1.7.36 - 2.20.1 2.3.1 - 2.6 - 1.5.4 - 2.18.1 - 2.22.2 - 2.10.0 3.3.1 - 2.1.3 - - net.minidev - json-smart - ${json-smart.version} - org.apache.kafka kafka_2.13 @@ -122,23 +115,58 @@ org.testcontainers testcontainers + ${testcontainers.version} - - io.qameta.allure - allure-junit5 - ${allure.version} + org.testcontainers + selenium + ${testcontainers.version} + + + org.apache.httpcomponents.core5 + httpcore5 + ${httpcomponents.version} + + + org.apache.httpcomponents.client5 + httpclient5 + ${httpcomponents.version} + + + org.seleniumhq.selenium + selenium-http + ${selenium.version} com.codeborne selenide ${selenide.version} + + org.testng + testng + ${testng.version} + io.qameta.allure allure-selenide ${allure.version} + + io.qameta.allure + allure-testng + ${allure.version} + + + io.qase + qase-testng + ${qase.io.version} + + + io.qase + qase-api + ${qase.io.version} + org.hamcrest hamcrest @@ -150,20 +178,9 @@ ${assertj.version} - com.google.auto.service - auto-service - ${google.auto-service.version} - - - org.junit.jupiter - junit-jupiter-api - ${junit.version} - - - org.junit.jupiter - junit-jupiter-engine - ${junit.version} - test + org.aspectj + aspectjrt + ${aspectj.version} org.slf4j @@ -175,66 +192,16 @@ lombok ${org.projectlombok.version} - - org.aspectj - aspectjrt - ${aspectj.version} - - - - org.testcontainers - junit-jupiter - - - io.qameta.allure - allure-java-commons - ${allure.java-commons.version} - io.github.cdimascio dotenv-java ${dotenv.version} - - org.junit.platform - junit-platform-launcher - - - ru.yandex.qatools.allure - allure-maven-plugin - ${allure.maven-plugin.version} - - - ru.yandex.qatools.ashot - ashot - ${ashot.version} - - - org.seleniumhq.selenium - selenium-remote-driver - - - - - io.qameta.allure.plugins - screen-diff-plugin - ${allure.screendiff-plugin.version} - com.provectus kafka-ui-contract ${kafka-ui-contract} - - org.testcontainers - selenium - test - - - io.qase - qase-api - ${qase.io.version} - @@ -252,6 +219,13 @@ true + + + org.apache.maven.surefire + surefire-testng + ${maven.surefire-plugin.version} + + org.apache.maven.plugins @@ -269,18 +243,16 @@ maven-surefire-plugin ${maven.surefire-plugin.version} - false - - - junit.jupiter.extensions.autodetection.enabled - true - - -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" + + org.apache.maven.surefire + surefire-testng + ${maven.surefire-plugin.version} + org.aspectj aspectjweaver @@ -291,11 +263,7 @@ io.qameta.allure allure-maven - ${allure-maven.version} - - - org.apache.maven.plugins - maven-compiler-plugin + 2.10.0 diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Connector.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Connector.java index 9e30ba9f19..48088cdf91 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Connector.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Connector.java @@ -8,5 +8,4 @@ import lombok.experimental.Accessors; public class Connector { private String name, config; - } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Schema.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Schema.java index a9c7dca75b..cd573037ba 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Schema.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Schema.java @@ -4,28 +4,28 @@ import com.provectus.kafka.ui.api.model.SchemaType; import lombok.Data; import lombok.experimental.Accessors; -import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; @Data @Accessors(chain = true) public class Schema { - private String name,valuePath; + private String name, valuePath; private SchemaType type; - public static Schema createSchemaAvro(){ + public static Schema createSchemaAvro() { return new Schema().setName("schema_avro-" + randomAlphabetic(5)) .setType(SchemaType.AVRO) .setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schema_avro_value.json"); } - public static Schema createSchemaJson(){ + public static Schema createSchemaJson() { return new Schema().setName("schema_json-" + randomAlphabetic(5)) .setType(SchemaType.JSON) .setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schema_Json_Value.json"); } - public static Schema createSchemaProtobuf(){ + public static Schema createSchemaProtobuf() { return new Schema().setName("schema_protobuf-" + randomAlphabetic(5)) .setType(SchemaType.PROTOBUF) .setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schema_protobuf_value.txt"); diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java index 53c234c8d3..21486a93f1 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java @@ -1,17 +1,20 @@ package com.provectus.kafka.ui.models; -import com.provectus.kafka.ui.pages.topic.enums.CleanupPolicyValue; -import com.provectus.kafka.ui.pages.topic.enums.CustomParameterType; -import com.provectus.kafka.ui.pages.topic.enums.MaxSizeOnDisk; +import com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue; +import com.provectus.kafka.ui.pages.topics.enums.CustomParameterType; +import com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk; +import com.provectus.kafka.ui.pages.topics.enums.TimeToRetain; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) public class Topic { + private String name, timeToRetainData, maxMessageBytes, messageKey, messageContent, customParameterValue; private int numberOfPartitions; private CustomParameterType customParameterType; private CleanupPolicyValue cleanupPolicyValue; private MaxSizeOnDisk maxSizeOnDisk; + private TimeToRetain timeToRetain; } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java index 8b12d3b0d3..c131bd8e0d 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java @@ -1,92 +1,111 @@ package com.provectus.kafka.ui.pages; -import static com.codeborne.selenide.Selenide.$$x; -import static com.codeborne.selenide.Selenide.$x; - import com.codeborne.selenide.Condition; import com.codeborne.selenide.ElementsCollection; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.utilities.WebUtils; -import java.time.Duration; import lombok.extern.slf4j.Slf4j; +import java.time.Duration; + +import static com.codeborne.selenide.Selenide.$$x; +import static com.codeborne.selenide.Selenide.$x; + @Slf4j public abstract class BasePage extends WebUtils { - protected SelenideElement loadingSpinner = $x("//div[@role='progressbar']"); - protected SelenideElement submitBtn = $x("//button[@type='submit']"); - protected SelenideElement tableGrid = $x("//table"); - protected SelenideElement dotMenuBtn = $x("//button[@aria-label='Dropdown Toggle']"); - protected SelenideElement alertHeader = $x("//div[@role='alert']//div[@role='heading']"); - protected SelenideElement alertMessage = $x("//div[@role='alert']//div[@role='contentinfo']"); - protected SelenideElement confirmBtn = $x("//button[contains(text(),'Confirm')]"); - protected ElementsCollection allGridItems = $$x("//tr[@class]"); - protected String summaryCellLocator = "//div[contains(text(),'%s')]"; - protected String tableElementNameLocator = "//tbody//a[contains(text(),'%s')]"; - protected String columnHeaderLocator = "//table//tr/th//div[text()='%s']"; + protected SelenideElement loadingSpinner = $x("//div[@role='progressbar']"); + protected SelenideElement submitBtn = $x("//button[@type='submit']"); + protected SelenideElement tableGrid = $x("//table"); + protected SelenideElement dotMenuBtn = $x("//button[@aria-label='Dropdown Toggle']"); + protected SelenideElement alertHeader = $x("//div[@role='alert']//div[@role='heading']"); + protected SelenideElement alertMessage = $x("//div[@role='alert']//div[@role='contentinfo']"); + protected SelenideElement confirmationMdl = $x("//div[text()= 'Confirm the action']/.."); + protected SelenideElement confirmBtn = $x("//button[contains(text(),'Confirm')]"); + protected SelenideElement cancelBtn = $x("//button[contains(text(),'Cancel')]"); + protected SelenideElement backBtn = $x("//button[contains(text(),'Back')]"); + protected SelenideElement nextBtn = $x("//button[contains(text(),'Next')]"); + protected ElementsCollection ddlOptions = $$x("//li[@value]"); + protected ElementsCollection gridItems = $$x("//tr[@class]"); + protected String summaryCellLocator = "//div[contains(text(),'%s')]"; + protected String tableElementNameLocator = "//tbody//a[contains(text(),'%s')]"; + protected String columnHeaderLocator = "//table//tr/th//div[text()='%s']"; - protected void waitUntilSpinnerDisappear() { - log.debug("\nwaitUntilSpinnerDisappear"); - if(isVisible(loadingSpinner)){ - loadingSpinner.shouldBe(Condition.disappear, Duration.ofSeconds(30)); - } - } - - protected void clickSubmitBtn() { - clickByJavaScript(submitBtn); - } - - protected SelenideElement getTableElement(String elementName) { - log.debug("\ngetTableElement: {}", elementName); - return $x(String.format(tableElementNameLocator, elementName)); - } - - protected String getAlertHeader() { - log.debug("\ngetAlertHeader"); - String result = alertHeader.shouldBe(Condition.visible).getText(); - log.debug("-> {}", result); - return result; - } - - protected String getAlertMessage() { - log.debug("\ngetAlertMessage"); - String result = alertMessage.shouldBe(Condition.visible).getText(); - log.debug("-> {}", result); - return result; - } - - protected boolean isAlertVisible(AlertHeader header) { - log.debug("\nisAlertVisible: {}", header.toString()); - boolean result = getAlertHeader().equals(header.toString()); - log.debug("-> {}", result); - return result; - } - - protected boolean isAlertVisible(AlertHeader header, String message) { - log.debug("\nisAlertVisible: {} {}", header, message); - boolean result = isAlertVisible(header) && getAlertMessage().equals(message); - log.debug("-> {}", result); - return result; - } - - protected void clickConfirmButton() { - confirmBtn.shouldBe(Condition.enabled).click(); - confirmBtn.shouldBe(Condition.disappear); - } - - public enum AlertHeader { - SUCCESS("Success"), - VALIDATION_ERROR("Validation Error"), - BAD_REQUEST("400 Bad Request"); - - private final String value; - - AlertHeader(String value) { - this.value = value; + protected void waitUntilSpinnerDisappear() { + log.debug("\nwaitUntilSpinnerDisappear"); + if (isVisible(loadingSpinner)) { + loadingSpinner.shouldBe(Condition.disappear, Duration.ofSeconds(30)); + } } - public String toString() { - return value; + protected void clickSubmitBtn() { + clickByJavaScript(submitBtn); + } + + protected SelenideElement getTableElement(String elementName) { + log.debug("\ngetTableElement: {}", elementName); + return $x(String.format(tableElementNameLocator, elementName)); + } + + protected ElementsCollection getDdlOptions() { + return ddlOptions; + } + + protected String getAlertHeader() { + log.debug("\ngetAlertHeader"); + String result = alertHeader.shouldBe(Condition.visible).getText(); + log.debug("-> {}", result); + return result; + } + + protected String getAlertMessage() { + log.debug("\ngetAlertMessage"); + String result = alertMessage.shouldBe(Condition.visible).getText(); + log.debug("-> {}", result); + return result; + } + + protected boolean isAlertVisible(AlertHeader header) { + log.debug("\nisAlertVisible: {}", header.toString()); + boolean result = getAlertHeader().equals(header.toString()); + log.debug("-> {}", result); + return result; + } + + protected boolean isAlertVisible(AlertHeader header, String message) { + log.debug("\nisAlertVisible: {} {}", header, message); + boolean result = isAlertVisible(header) && getAlertMessage().equals(message); + log.debug("-> {}", result); + return result; + } + + protected void clickConfirmButton() { + confirmBtn.shouldBe(Condition.enabled).click(); + confirmBtn.shouldBe(Condition.disappear); + } + + protected void clickCancelButton() { + cancelBtn.shouldBe(Condition.enabled).click(); + cancelBtn.shouldBe(Condition.disappear); + } + + protected boolean isConfirmationModalVisible() { + return isVisible(confirmationMdl); + } + + public enum AlertHeader { + SUCCESS("Success"), + VALIDATION_ERROR("Validation Error"), + BAD_REQUEST("400 Bad Request"); + + private final String value; + + AlertHeader(String value) { + this.value = value; + } + + public String toString() { + return value; + } } - } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/NaviSideBar.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/NaviSideBar.java index b70bf7755a..3c5b0fe6d9 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/NaviSideBar.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/NaviSideBar.java @@ -1,16 +1,17 @@ package com.provectus.kafka.ui.pages; -import static com.codeborne.selenide.Selenide.$x; -import static com.provectus.kafka.ui.settings.BaseSource.CLUSTER_NAME; - import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import io.qameta.allure.Step; + import java.time.Duration; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.codeborne.selenide.Selenide.$x; +import static com.provectus.kafka.ui.settings.BaseSource.CLUSTER_NAME; + public class NaviSideBar extends BasePage { protected SelenideElement dashboardMenuItem = $x("//a[@title='Dashboard']"); @@ -45,6 +46,13 @@ public class NaviSideBar extends BasePage { return this; } + public List getAllMenuButtons() { + expandCluster(CLUSTER_NAME); + return Stream.of(SideMenuOption.values()) + .map(option -> $x(String.format(sideMenuOptionElementLocator, option.value))) + .collect(Collectors.toList()); + } + public enum SideMenuOption { DASHBOARD("Dashboard"), BROKERS("Brokers"), @@ -60,11 +68,4 @@ public class NaviSideBar extends BasePage { this.value = value; } } - - public List getAllMenuButtons() { - expandCluster(CLUSTER_NAME); - return Stream.of(SideMenuOption.values()) - .map(option -> $x(String.format(sideMenuOptionElementLocator, option.value))) - .collect(Collectors.toList()); - } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/TopPanel.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/TopPanel.java index 5e5a317647..28de4fe058 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/TopPanel.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/TopPanel.java @@ -1,12 +1,14 @@ package com.provectus.kafka.ui.pages; -import static com.codeborne.selenide.Selenide.$x; - import com.codeborne.selenide.SelenideElement; + import java.util.Arrays; import java.util.List; -public class TopPanel extends BasePage{ +import static com.codeborne.selenide.Selenide.$x; + +public class TopPanel extends BasePage { + protected SelenideElement kafkaLogo = $x("//a[contains(text(),'UI for Apache Kafka')]"); protected SelenideElement kafkaVersion = $x("//a[@title='Current commit']"); protected SelenideElement logOutBtn = $x("//button[contains(text(),'Log out')]"); diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersConfigTab.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersConfigTab.java index 70bba6047a..7b37d6709c 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersConfigTab.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersConfigTab.java @@ -1,40 +1,41 @@ package com.provectus.kafka.ui.pages.brokers; -import static com.codeborne.selenide.Selenide.$$x; -import static com.codeborne.selenide.Selenide.$x; - import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; + import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.codeborne.selenide.Selenide.$$x; +import static com.codeborne.selenide.Selenide.$x; + public class BrokersConfigTab extends BasePage { - protected List editBtn = $$x("//button[@aria-label='editAction']"); - protected SelenideElement searchByKeyField = $x("//input[@placeholder='Search by Key']"); + protected List editBtn = $$x("//button[@aria-label='editAction']"); + protected SelenideElement searchByKeyField = $x("//input[@placeholder='Search by Key']"); - @Step - public BrokersConfigTab waitUntilScreenReady(){ - waitUntilSpinnerDisappear(); - searchByKeyField.shouldBe(Condition.visible); - return this; - } + @Step + public BrokersConfigTab waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + searchByKeyField.shouldBe(Condition.visible); + return this; + } - @Step - public boolean isSearchByKeyVisible() { - return isVisible(searchByKeyField); - } + @Step + public boolean isSearchByKeyVisible() { + return isVisible(searchByKeyField); + } - public List getColumnHeaders() { - return Stream.of("Key", "Value", "Source") - .map(name -> $x(String.format(columnHeaderLocator, name))) - .collect(Collectors.toList()); - } + public List getColumnHeaders() { + return Stream.of("Key", "Value", "Source") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } - public List getEditButtons() { - return editBtn; - } + public List getEditButtons() { + return editBtn; + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersDetails.java index 4eca65f1f4..8cc3dd98ba 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersDetails.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersDetails.java @@ -1,91 +1,92 @@ package com.provectus.kafka.ui.pages.brokers; -import static com.codeborne.selenide.Selenide.$; -import static com.codeborne.selenide.Selenide.$x; - import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; +import org.openqa.selenium.By; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.openqa.selenium.By; + +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.$x; public class BrokersDetails extends BasePage { - protected SelenideElement logDirectoriesTab = $x("//a[text()='Log directories']"); - protected SelenideElement metricsTab = $x("//a[text()='Metrics']"); - protected String brokersTabLocator = "//a[text()='%s']"; + protected SelenideElement logDirectoriesTab = $x("//a[text()='Log directories']"); + protected SelenideElement metricsTab = $x("//a[text()='Metrics']"); + protected String brokersTabLocator = "//a[text()='%s']"; - @Step - public BrokersDetails waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - Arrays.asList(logDirectoriesTab, metricsTab).forEach(element -> element.shouldBe(Condition.visible)); - return this; - } - - @Step - public BrokersDetails openDetailsTab(DetailsTab menu) { - $(By.linkText(menu.toString())).shouldBe(Condition.enabled).click(); - waitUntilSpinnerDisappear(); - return this; - } - - private List getVisibleColumnHeaders() { - return Stream.of("Name", "Topics", "Error", "Partitions") - .map(name -> $x(String.format(columnHeaderLocator, name))) - .collect(Collectors.toList()); - } - - private List getEnabledColumnHeaders() { - return Stream.of("Name", "Error") - .map(name -> $x(String.format(columnHeaderLocator, name))) - .collect(Collectors.toList()); - } - - private List getVisibleSummaryCells() { - return Stream.of("Segment Size", "Segment Count", "Port", "Host") - .map(name -> $x(String.format(summaryCellLocator, name))) - .collect(Collectors.toList()); - } - - private List getDetailsTabs() { - return Stream.of(DetailsTab.values()) - .map(name -> $x(String.format(brokersTabLocator, name))) - .collect(Collectors.toList()); - } - - @Step - public List getAllEnabledElements() { - List enabledElements = new ArrayList<>(getEnabledColumnHeaders()); - enabledElements.addAll(getDetailsTabs()); - return enabledElements; - } - - @Step - public List getAllVisibleElements() { - List visibleElements = new ArrayList<>(getVisibleSummaryCells()); - visibleElements.addAll(getVisibleColumnHeaders()); - visibleElements.addAll(getDetailsTabs()); - return visibleElements; - } - - public enum DetailsTab { - LOG_DIRECTORIES("Log directories"), - CONFIGS("Configs"), - METRICS("Metrics"); - - private final String value; - - DetailsTab(String value) { - this.value = value; + @Step + public BrokersDetails waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + Arrays.asList(logDirectoriesTab, metricsTab).forEach(element -> element.shouldBe(Condition.visible)); + return this; } - public String toString() { - return value; + @Step + public BrokersDetails openDetailsTab(DetailsTab menu) { + $(By.linkText(menu.toString())).shouldBe(Condition.enabled).click(); + waitUntilSpinnerDisappear(); + return this; + } + + private List getVisibleColumnHeaders() { + return Stream.of("Name", "Topics", "Error", "Partitions") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } + + private List getEnabledColumnHeaders() { + return Stream.of("Name", "Error") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } + + private List getVisibleSummaryCells() { + return Stream.of("Segment Size", "Segment Count", "Port", "Host") + .map(name -> $x(String.format(summaryCellLocator, name))) + .collect(Collectors.toList()); + } + + private List getDetailsTabs() { + return Stream.of(DetailsTab.values()) + .map(name -> $x(String.format(brokersTabLocator, name))) + .collect(Collectors.toList()); + } + + @Step + public List getAllEnabledElements() { + List enabledElements = new ArrayList<>(getEnabledColumnHeaders()); + enabledElements.addAll(getDetailsTabs()); + return enabledElements; + } + + @Step + public List getAllVisibleElements() { + List visibleElements = new ArrayList<>(getVisibleSummaryCells()); + visibleElements.addAll(getVisibleColumnHeaders()); + visibleElements.addAll(getDetailsTabs()); + return visibleElements; + } + + public enum DetailsTab { + LOG_DIRECTORIES("Log directories"), + CONFIGS("Configs"), + METRICS("Metrics"); + + private final String value; + + DetailsTab(String value) { + this.value = value; + } + + public String toString() { + return value; + } } - } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java index 239a635934..3d3a58f355 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java @@ -1,123 +1,124 @@ package com.provectus.kafka.ui.pages.brokers; -import static com.codeborne.selenide.Selenide.$x; - import com.codeborne.selenide.CollectionCondition; import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; + import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.codeborne.selenide.Selenide.$x; + public class BrokersList extends BasePage { - protected SelenideElement brokersListHeader = $x("//h1[text()='Brokers']"); + protected SelenideElement brokersListHeader = $x("//h1[text()='Brokers']"); - @Step - public BrokersList waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - brokersListHeader.shouldBe(Condition.visible); - return this; - } - - @Step - public BrokersList openBroker(int brokerId) { - getBrokerItem(brokerId).openItem(); - return this; - } - - private List getUptimeSummaryCells() { - return Stream.of("Broker Count", "Active Controller", "Version") - .map(name -> $x(String.format(summaryCellLocator, name))) - .collect(Collectors.toList()); - } - - private List getPartitionsSummaryCells() { - return Stream.of("Online", "URP", "In Sync Replicas", "Out Of Sync Replicas") - .map(name -> $x(String.format(summaryCellLocator, name))) - .collect(Collectors.toList()); - } - - @Step - public List getAllVisibleElements() { - List visibleElements = new ArrayList<>(getUptimeSummaryCells()); - visibleElements.addAll(getPartitionsSummaryCells()); - return visibleElements; - } - - private List getEnabledColumnHeaders() { - return Stream.of("Broker ID", "Segment Size", "Segment Count", "Port", "Host") - .map(name -> $x(String.format(columnHeaderLocator, name))) - .collect(Collectors.toList()); - } - - @Step - public List getAllEnabledElements() { - return getEnabledColumnHeaders(); - } - - private List initGridItems() { - List gridItemList = new ArrayList<>(); - allGridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) - .forEach(item -> gridItemList.add(new BrokersList.BrokerGridItem(item))); - return gridItemList; - } - - @Step - public BrokerGridItem getBrokerItem(int id){ - return initGridItems().stream() - .filter(e ->e.getId() == id) - .findFirst().orElse(null); - } - - @Step - public List getAllBrokers(){ - return initGridItems(); - } - - public static class BrokerGridItem extends BasePage { - - private final SelenideElement element; - - public BrokerGridItem(SelenideElement element) { - this.element = element; - } - - private SelenideElement getIdElm() { - return element.$x("./td[1]/div/a"); + @Step + public BrokersList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + brokersListHeader.shouldBe(Condition.visible); + return this; } @Step - public int getId() { - return Integer.parseInt(getIdElm().getText().trim()); + public BrokersList openBroker(int brokerId) { + getBrokerItem(brokerId).openItem(); + return this; + } + + private List getUptimeSummaryCells() { + return Stream.of("Broker Count", "Active Controller", "Version") + .map(name -> $x(String.format(summaryCellLocator, name))) + .collect(Collectors.toList()); + } + + private List getPartitionsSummaryCells() { + return Stream.of("Online", "URP", "In Sync Replicas", "Out Of Sync Replicas") + .map(name -> $x(String.format(summaryCellLocator, name))) + .collect(Collectors.toList()); } @Step - public void openItem() { - getIdElm().click(); + public List getAllVisibleElements() { + List visibleElements = new ArrayList<>(getUptimeSummaryCells()); + visibleElements.addAll(getPartitionsSummaryCells()); + return visibleElements; + } + + private List getEnabledColumnHeaders() { + return Stream.of("Broker ID", "Segment Size", "Segment Count", "Port", "Host") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); } @Step - public int getSegmentSize(){ - return Integer.parseInt(element.$x("./td[2]").getText().trim()); + public List getAllEnabledElements() { + return getEnabledColumnHeaders(); + } + + private List initGridItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new BrokersList.BrokerGridItem(item))); + return gridItemList; } @Step - public int getSegmentCount(){ - return Integer.parseInt(element.$x("./td[3]").getText().trim()); + public BrokerGridItem getBrokerItem(int id) { + return initGridItems().stream() + .filter(e -> e.getId() == id) + .findFirst().orElseThrow(); } @Step - public int getPort(){ - return Integer.parseInt(element.$x("./td[4]").getText().trim()); + public List getAllBrokers() { + return initGridItems(); } - @Step - public String getHost(){ - return element.$x("./td[5]").getText().trim(); + public static class BrokerGridItem extends BasePage { + + private final SelenideElement element; + + public BrokerGridItem(SelenideElement element) { + this.element = element; + } + + private SelenideElement getIdElm() { + return element.$x("./td[1]/div/a"); + } + + @Step + public int getId() { + return Integer.parseInt(getIdElm().getText().trim()); + } + + @Step + public void openItem() { + getIdElm().click(); + } + + @Step + public int getSegmentSize() { + return Integer.parseInt(element.$x("./td[2]").getText().trim()); + } + + @Step + public int getSegmentCount() { + return Integer.parseInt(element.$x("./td[3]").getText().trim()); + } + + @Step + public int getPort() { + return Integer.parseInt(element.$x("./td[4]").getText().trim()); + } + + @Step + public String getHost() { + return element.$x("./td[5]").getText().trim(); + } } - } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connector/ConnectorCreateForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorCreateForm.java similarity index 89% rename from kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connector/ConnectorCreateForm.java rename to kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorCreateForm.java index 093341cd64..7bc2aa88aa 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connector/ConnectorCreateForm.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorCreateForm.java @@ -1,12 +1,12 @@ -package com.provectus.kafka.ui.pages.connector; - -import static com.codeborne.selenide.Selenide.$x; +package com.provectus.kafka.ui.pages.connectors; import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; +import static com.codeborne.selenide.Selenide.$x; + public class ConnectorCreateForm extends BasePage { protected SelenideElement nameField = $x("//input[@name='name']"); @@ -31,8 +31,8 @@ public class ConnectorCreateForm extends BasePage { @Step public ConnectorCreateForm clickSubmitButton() { - clickSubmitBtn(); - waitUntilSpinnerDisappear(); - return this; + clickSubmitBtn(); + waitUntilSpinnerDisappear(); + return this; } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connector/ConnectorDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorDetails.java similarity index 62% rename from kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connector/ConnectorDetails.java rename to kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorDetails.java index 291c4aa6f3..fbe1984ce3 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connector/ConnectorDetails.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorDetails.java @@ -1,28 +1,28 @@ -package com.provectus.kafka.ui.pages.connector; - -import static com.codeborne.selenide.Selenide.$x; +package com.provectus.kafka.ui.pages.connectors; import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; +import static com.codeborne.selenide.Selenide.$x; + public class ConnectorDetails extends BasePage { - protected SelenideElement deleteBtn = $x("//li/div[contains(text(),'Delete')]"); - protected SelenideElement confirmBtnMdl = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]"); - protected SelenideElement contentTextArea = $x("//textarea[@class='ace_text-input']"); - protected SelenideElement taskTab = $x("//a[contains(text(),'Tasks')]"); - protected SelenideElement configTab = $x("//a[contains(text(),'Config')]"); - protected SelenideElement configField = $x("//div[@id='config']"); - protected String connectorHeaderLocator = "//h1[contains(text(),'%s')]"; + protected SelenideElement deleteBtn = $x("//li/div[contains(text(),'Delete')]"); + protected SelenideElement confirmBtnMdl = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]"); + protected SelenideElement contentTextArea = $x("//textarea[@class='ace_text-input']"); + protected SelenideElement taskTab = $x("//a[contains(text(),'Tasks')]"); + protected SelenideElement configTab = $x("//a[contains(text(),'Config')]"); + protected SelenideElement configField = $x("//div[@id='config']"); + protected String connectorHeaderLocator = "//h1[contains(text(),'%s')]"; - @Step - public ConnectorDetails waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - dotMenuBtn.shouldBe(Condition.visible); - return this; - } + @Step + public ConnectorDetails waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + dotMenuBtn.shouldBe(Condition.visible); + return this; + } @Step public ConnectorDetails openConfigTab() { @@ -41,8 +41,8 @@ public class ConnectorDetails extends BasePage { @Step public ConnectorDetails clickSubmitButton() { - clickSubmitBtn(); - return this; + clickSubmitBtn(); + return this; } @Step @@ -74,11 +74,11 @@ public class ConnectorDetails extends BasePage { @Step public boolean isConnectorHeaderVisible(String connectorName) { - return isVisible($x(String.format(connectorHeaderLocator,connectorName))); + return isVisible($x(String.format(connectorHeaderLocator, connectorName))); } @Step - public boolean isAlertWithMessageVisible(AlertHeader header, String message){ - return isAlertVisible(header, message); + public boolean isAlertWithMessageVisible(AlertHeader header, String message) { + return isAlertVisible(header, message); } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connector/KafkaConnectList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/KafkaConnectList.java similarity index 77% rename from kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connector/KafkaConnectList.java rename to kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/KafkaConnectList.java index b86734b2dc..3be1826511 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connector/KafkaConnectList.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/KafkaConnectList.java @@ -1,26 +1,26 @@ -package com.provectus.kafka.ui.pages.connector; - -import static com.codeborne.selenide.Selenide.$x; +package com.provectus.kafka.ui.pages.connectors; import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; +import static com.codeborne.selenide.Selenide.$x; + public class KafkaConnectList extends BasePage { protected SelenideElement createConnectorBtn = $x("//button[contains(text(),'Create Connector')]"); - public KafkaConnectList(){ + public KafkaConnectList() { tableElementNameLocator = "//tbody//td[contains(text(),'%s')]"; } @Step public KafkaConnectList waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - createConnectorBtn.shouldBe(Condition.visible); - return this; + waitUntilSpinnerDisappear(); + createConnectorBtn.shouldBe(Condition.visible); + return this; } @Step @@ -31,7 +31,7 @@ public class KafkaConnectList extends BasePage { @Step public KafkaConnectList openConnector(String connectorName) { - getTableElement(connectorName).shouldBe(Condition.enabled).click(); + getTableElement(connectorName).shouldBe(Condition.enabled).click(); return this; } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumer/ConsumersList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumer/ConsumersList.java deleted file mode 100644 index 07824736be..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumer/ConsumersList.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.provectus.kafka.ui.pages.consumer; - -import static com.codeborne.selenide.Selenide.$x; - -import com.codeborne.selenide.SelenideElement; -import com.provectus.kafka.ui.pages.BasePage; -public class ConsumersList extends BasePage { - - protected SelenideElement consumerListHeader = $x("//h1[text()='Consumers']"); -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumer/ConsumersDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersDetails.java similarity index 95% rename from kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumer/ConsumersDetails.java rename to kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersDetails.java index dabdff88ec..240dc613c4 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumer/ConsumersDetails.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersDetails.java @@ -1,11 +1,11 @@ -package com.provectus.kafka.ui.pages.consumer; - -import static com.codeborne.selenide.Selenide.$x; +package com.provectus.kafka.ui.pages.consumers; import com.codeborne.selenide.Condition; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; +import static com.codeborne.selenide.Selenide.$x; + public class ConsumersDetails extends BasePage { protected String consumerIdHeaderLocator = "//h1[contains(text(),'%s')]"; diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersList.java new file mode 100644 index 0000000000..6d0c1d48f7 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersList.java @@ -0,0 +1,20 @@ +package com.provectus.kafka.ui.pages.consumers; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; + +import static com.codeborne.selenide.Selenide.$x; + +public class ConsumersList extends BasePage { + + protected SelenideElement consumerListHeader = $x("//h1[text()='Consumers']"); + + @Step + public ConsumersList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + consumerListHeader.shouldHave(Condition.visible); + return this; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlDbList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlDbList.java new file mode 100644 index 0000000000..25246a86ed --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlDbList.java @@ -0,0 +1,137 @@ +package com.provectus.kafka.ui.pages.ksqlDb; + +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.$x; + +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import com.provectus.kafka.ui.pages.ksqlDb.enums.KsqlMenuTabs; +import io.qameta.allure.Step; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.openqa.selenium.By; + +public class KsqlDbList extends BasePage { + protected SelenideElement executeKsqlBtn = $x("//button[text()='Execute KSQL Request']"); + protected SelenideElement tablesTab = $x("//nav[@role='navigation']/a[text()='Tables']"); + protected SelenideElement streamsTab = $x("//nav[@role='navigation']/a[text()='Streams']"); + + @Step + public KsqlDbList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + Arrays.asList(tablesTab, streamsTab).forEach(tab -> tab.shouldBe(Condition.visible)); + return this; + } + + @Step + public KsqlDbList clickExecuteKsqlRequestBtn() { + clickByJavaScript(executeKsqlBtn); + return this; + } + + @Step + public KsqlDbList openDetailsTab(KsqlMenuTabs menu) { + $(By.linkText(menu.toString())).shouldBe(Condition.visible).click(); + waitUntilSpinnerDisappear(); + return this; + } + + private List initTablesItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new KsqlDbList.KsqlTablesGridItem(item))); + return gridItemList; + } + + @Step + public KsqlDbList.KsqlTablesGridItem getTableByName(String tableName) { + return initTablesItems().stream() + .filter(e -> e.getTableName().equals(tableName)) + .findFirst().orElseThrow(); + } + + public static class KsqlTablesGridItem extends BasePage { + + private final SelenideElement element; + + public KsqlTablesGridItem(SelenideElement element) { + this.element = element; + } + + @Step + public String getTableName() { + return element.$x("./td[1]").getText().trim(); + } + + @Step + public String getTopicName() { + return element.$x("./td[2]").getText().trim(); + } + + @Step + public String getKeyFormat() { + return element.$x("./td[3]").getText().trim(); + } + + @Step + public String getValueFormat() { + return element.$x("./td[4]").getText().trim(); + } + + @Step + public String getIsWindowed() { + return element.$x("./td[5]").getText().trim(); + } + } + + private List initStreamsItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new KsqlDbList.KsqlStreamsGridItem(item))); + return gridItemList; + } + + @Step + public KsqlDbList.KsqlStreamsGridItem getStreamByName(String streamName) { + return initStreamsItems().stream() + .filter(e -> e.getStreamName().equals(streamName)) + .findFirst().orElseThrow(); + } + + public static class KsqlStreamsGridItem extends BasePage { + + private final SelenideElement element; + + public KsqlStreamsGridItem(SelenideElement element) { + this.element = element; + } + + @Step + public String getStreamName() { + return element.$x("./td[1]").getText().trim(); + } + + @Step + public String getTopicName() { + return element.$x("./td[2]").getText().trim(); + } + + @Step + public String getKeyFormat() { + return element.$x("./td[3]").getText().trim(); + } + + @Step + public String getValueFormat() { + return element.$x("./td[4]").getText().trim(); + } + + @Step + public String getIsWindowed() { + return element.$x("./td[5]").getText().trim(); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlQueryForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlQueryForm.java new file mode 100644 index 0000000000..30ac1007fd --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlQueryForm.java @@ -0,0 +1,155 @@ +package com.provectus.kafka.ui.pages.ksqlDb; + +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.ElementsCollection; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import static com.codeborne.selenide.Condition.visible; +import static com.codeborne.selenide.Selenide.$$x; +import static com.codeborne.selenide.Selenide.$x; + +public class KsqlQueryForm extends BasePage { + protected SelenideElement pageTitle = $x("//h1[text()='Query']"); + protected SelenideElement clearBtn = $x("//div/button[text()='Clear']"); + protected SelenideElement executeBtn = $x("//div/button[text()='Execute']"); + protected SelenideElement stopQueryBtn = $x("//div/button[text()='Stop query']"); + protected SelenideElement clearResultsBtn = $x("//div/button[text()='Clear results']"); + protected SelenideElement addStreamPropertyBtn = $x("//button[text()='Add Stream Property']"); + protected SelenideElement queryAreaValue = $x("//div[@class='ace_content']"); + protected SelenideElement queryArea = $x("//div[@id='ksql']/textarea[@class='ace_text-input']"); + protected ElementsCollection ksqlGridItems = $$x("//tbody//tr"); + protected ElementsCollection keyField = $$x("//input[@aria-label='key']"); + protected ElementsCollection valueField = $$x("//input[@aria-label='value']"); + + @Step + public KsqlQueryForm waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + pageTitle.shouldBe(Condition.visible); + return this; + } + + @Step + public KsqlQueryForm clickClearBtn() { + clickByJavaScript(clearBtn); + return this; + } + + @Step + public KsqlQueryForm clickExecuteBtn() { + clickByActions(executeBtn); + if (queryAreaValue.getText().contains("EMIT CHANGES;")) { + loadingSpinner.shouldBe(Condition.visible); + } else { + waitUntilSpinnerDisappear(); + } + return this; + } + + @Step + public KsqlQueryForm clickStopQueryBtn() { + clickByActions(stopQueryBtn); + waitUntilSpinnerDisappear(); + return this; + } + + @Step + public KsqlQueryForm clickClearResultsBtn() { + clickByActions(clearResultsBtn); + waitUntilSpinnerDisappear(); + return this; + } + + @Step + public KsqlQueryForm clickAddStreamProperty() { + clickByJavaScript(addStreamPropertyBtn); + return this; + } + + @Step + public KsqlQueryForm setQuery(String query) { + queryAreaValue.shouldBe(Condition.visible).click(); + queryArea.setValue(query); + return this; + } + + @Step + public KsqlQueryForm.KsqlResponseGridItem getTableByName(String name) { + return initItems().stream() + .filter(e -> e.getName().equalsIgnoreCase(name)) + .findFirst().orElseThrow(); + } + + @Step + public boolean areResultsVisible() { + boolean visible = false; + try { + visible = initItems().size() > 0; + } catch (Throwable ignored) { + } + return visible; + } + + private List initItems() { + List gridItemList = new ArrayList<>(); + ksqlGridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new KsqlQueryForm.KsqlResponseGridItem(item))); + return gridItemList; + } + + public static class KsqlResponseGridItem extends BasePage { + + private final SelenideElement element; + + private KsqlResponseGridItem(SelenideElement element) { + this.element = element; + } + + @Step + public String getType() { + return element.$x("./td[1]").getText().trim(); + } + + @Step + public String getName() { + return element.$x("./td[2]").scrollTo().getText().trim(); + } + + @Step + public boolean isVisible() { + boolean isVisible = false; + try { + element.$x("./td[2]").shouldBe(visible, Duration.ofMillis(500)); + isVisible = true; + } catch (Throwable ignored) { + } + return isVisible; + } + + @Step + public String getTopic() { + return element.$x("./td[3]").getText().trim(); + } + + @Step + public String getKeyFormat() { + return element.$x("./td[4]").getText().trim(); + } + + @Step + public String getValueFormat() { + return element.$x("./td[5]").getText().trim(); + } + + @Step + public String getIsWindowed() { + return element.$x("./td[6]").getText().trim(); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/enums/KsqlMenuTabs.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/enums/KsqlMenuTabs.java new file mode 100644 index 0000000000..bb719dc0f6 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/enums/KsqlMenuTabs.java @@ -0,0 +1,17 @@ +package com.provectus.kafka.ui.pages.ksqlDb.enums; + +public enum KsqlMenuTabs { + + TABLES("Table"), + STREAMS("Streams"); + + private final String value; + + KsqlMenuTabs(String value) { + this.value = value; + } + + public String toString() { + return value; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/enums/KsqlQueryConfig.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/enums/KsqlQueryConfig.java new file mode 100644 index 0000000000..9f85837474 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/enums/KsqlQueryConfig.java @@ -0,0 +1,19 @@ +package com.provectus.kafka.ui.pages.ksqlDb.enums; + +public enum KsqlQueryConfig { + + SHOW_TABLES("show tables;"), + SHOW_STREAMS("show streams;"), + SELECT_ALL_FROM("SELECT * FROM %s\n" + + "EMIT CHANGES;"); + + private final String query; + + KsqlQueryConfig(String query) { + this.query = query; + } + + public String getQuery() { + return query; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/models/Stream.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/models/Stream.java new file mode 100644 index 0000000000..4030a478c4 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/models/Stream.java @@ -0,0 +1,11 @@ +package com.provectus.kafka.ui.pages.ksqlDb.models; + +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class Stream { + + private String name, topicName, valueFormat, partitions; +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/models/Table.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/models/Table.java new file mode 100644 index 0000000000..1856fffd85 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/models/Table.java @@ -0,0 +1,11 @@ +package com.provectus.kafka.ui.pages.ksqlDb.models; + +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class Table { + + private String name, streamName; +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schema/SchemaCreateForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaCreateForm.java similarity index 76% rename from kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schema/SchemaCreateForm.java rename to kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaCreateForm.java index d749b5feb8..b823b6b992 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schema/SchemaCreateForm.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaCreateForm.java @@ -1,8 +1,4 @@ -package com.provectus.kafka.ui.pages.schema; - -import static com.codeborne.selenide.Selenide.$; -import static com.codeborne.selenide.Selenide.$$x; -import static com.codeborne.selenide.Selenide.$x; +package com.provectus.kafka.ui.pages.schemas; import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; @@ -10,10 +6,13 @@ import com.provectus.kafka.ui.api.model.CompatibilityLevel; import com.provectus.kafka.ui.api.model.SchemaType; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; + import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.codeborne.selenide.Selenide.*; + public class SchemaCreateForm extends BasePage { protected SelenideElement schemaNameField = $x("//input[@name='subject']"); @@ -30,7 +29,7 @@ public class SchemaCreateForm extends BasePage { protected String ddlElementLocator = "//li[@value='%s']"; @Step - public SchemaCreateForm waitUntilScreenReady(){ + public SchemaCreateForm waitUntilScreenReady() { waitUntilSpinnerDisappear(); pageTitle.shouldBe(Condition.visible); return this; @@ -69,25 +68,25 @@ public class SchemaCreateForm extends BasePage { } @Step - public SchemaCreateForm openSchemaVersionDdl(){ - schemaVersionDdl.shouldBe(Condition.enabled).click(); - return this; + public SchemaCreateForm openSchemaVersionDdl() { + schemaVersionDdl.shouldBe(Condition.enabled).click(); + return this; } @Step - public int getVersionsNumberFromList(){ - return elementsCompareVersionDdl.size(); + public int getVersionsNumberFromList() { + return elementsCompareVersionDdl.size(); } @Step - public SchemaCreateForm selectVersionFromDropDown(int versionNumberDd){ - $x(String.format(ddlElementLocator,versionNumberDd)).shouldBe(Condition.visible).click(); - return this; + public SchemaCreateForm selectVersionFromDropDown(int versionNumberDd) { + $x(String.format(ddlElementLocator, versionNumberDd)).shouldBe(Condition.visible).click(); + return this; } @Step - public int getMarkedLinesNumber(){ - return visibleMarkers.size(); + public int getMarkedLinesNumber() { + return visibleMarkers.size(); } @Step @@ -100,23 +99,22 @@ public class SchemaCreateForm extends BasePage { @Step public List getAllDetailsPageElements() { - return Stream.of(compatibilityLevelList, newSchemaTextArea, latestSchemaTextArea, submitBtn, schemaTypeDdl) - .collect(Collectors.toList()); + return Stream.of(compatibilityLevelList, newSchemaTextArea, latestSchemaTextArea, submitBtn, schemaTypeDdl) + .collect(Collectors.toList()); } @Step - public boolean isSubmitBtnEnabled(){ - return isEnabled(submitBtn); + public boolean isSubmitBtnEnabled() { + return isEnabled(submitBtn); } @Step - public boolean isSchemaDropDownEnabled(){ + public boolean isSchemaDropDownEnabled() { boolean enabled = true; - try{ + try { String attribute = schemaTypeDdl.getAttribute("disabled"); enabled = false; - } - catch (Throwable ignored){ + } catch (Throwable ignored) { } return enabled; } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schema/SchemaDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaDetails.java similarity index 82% rename from kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schema/SchemaDetails.java rename to kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaDetails.java index ce21133709..fc7013d46f 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schema/SchemaDetails.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaDetails.java @@ -1,12 +1,12 @@ -package com.provectus.kafka.ui.pages.schema; - -import static com.codeborne.selenide.Selenide.$x; +package com.provectus.kafka.ui.pages.schemas; import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; +import static com.codeborne.selenide.Selenide.$x; + public class SchemaDetails extends BasePage { protected SelenideElement actualVersionTextArea = $x("//div[@id='schema']"); @@ -33,12 +33,12 @@ public class SchemaDetails extends BasePage { @Step public boolean isSchemaHeaderVisible(String schemaName) { - return isVisible($x(String.format(schemaHeaderLocator,schemaName))); + return isVisible($x(String.format(schemaHeaderLocator, schemaName))); } @Step - public int getLatestVersion(){ - return Integer.parseInt(latestVersionField.getText()); + public int getLatestVersion() { + return Integer.parseInt(latestVersionField.getText()); } @Step @@ -47,15 +47,15 @@ public class SchemaDetails extends BasePage { } @Step - public SchemaDetails openEditSchema(){ + public SchemaDetails openEditSchema() { editSchemaBtn.shouldBe(Condition.visible).click(); return this; } @Step - public SchemaDetails openCompareVersionMenu(){ - compareVersionBtn.shouldBe(Condition.enabled).click(); - return this; + public SchemaDetails openCompareVersionMenu() { + compareVersionBtn.shouldBe(Condition.enabled).click(); + return this; } @Step diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schema/SchemaRegistryList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaRegistryList.java similarity index 90% rename from kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schema/SchemaRegistryList.java rename to kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaRegistryList.java index e6476ab1f6..8f65947734 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schema/SchemaRegistryList.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaRegistryList.java @@ -1,18 +1,18 @@ -package com.provectus.kafka.ui.pages.schema; - -import static com.codeborne.selenide.Selenide.$x; +package com.provectus.kafka.ui.pages.schemas; import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; +import static com.codeborne.selenide.Selenide.$x; + public class SchemaRegistryList extends BasePage { protected SelenideElement createSchemaBtn = $x("//button[contains(text(),'Create Schema')]"); @Step - public SchemaRegistryList waitUntilScreenReady(){ + public SchemaRegistryList waitUntilScreenReady() { waitUntilSpinnerDisappear(); createSchemaBtn.shouldBe(Condition.visible); return this; diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicCreateEditForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicCreateEditForm.java deleted file mode 100644 index 3822cca257..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicCreateEditForm.java +++ /dev/null @@ -1,287 +0,0 @@ -package com.provectus.kafka.ui.pages.topic; - -import static com.codeborne.selenide.Selenide.$; -import static com.codeborne.selenide.Selenide.$$; -import static com.codeborne.selenide.Selenide.$x; -import static org.assertj.core.api.Assertions.assertThat; - -import com.codeborne.selenide.ClickOptions; -import com.codeborne.selenide.Condition; -import com.codeborne.selenide.ElementsCollection; -import com.codeborne.selenide.SelenideElement; -import com.provectus.kafka.ui.pages.BasePage; -import com.provectus.kafka.ui.pages.topic.enums.CleanupPolicyValue; -import com.provectus.kafka.ui.pages.topic.enums.CustomParameterType; -import com.provectus.kafka.ui.pages.topic.enums.MaxSizeOnDisk; -import io.qameta.allure.Step; - -public class TopicCreateEditForm extends BasePage { - - protected SelenideElement timeToRetainField = $x("//input[@id='timeToRetain']"); - protected SelenideElement partitionsField = $x("//input[@name='partitions']"); - protected SelenideElement nameField = $x("//input[@name='name']"); - protected SelenideElement maxMessageBytesField = $x("//input[@name='maxMessageBytes']"); - protected SelenideElement minInSyncReplicasField = $x("//input[@name='minInSyncReplicas']"); - protected SelenideElement cleanUpPolicyDdl = $x("//ul[@id='topicFormCleanupPolicy']"); - protected SelenideElement maxSizeOnDiscDdl = $x("//ul[@id='topicFormRetentionBytes']"); - protected SelenideElement customParameterDdl = $x("//ul[contains(@name,'customParams')]"); - protected SelenideElement deleteCustomParameterBtn = $x("//span[contains(@title,'Delete customParam')]"); - protected SelenideElement addCustomParameterTypeBtn = $x("//button[contains(text(),'Add Custom Parameter')]"); - protected SelenideElement customParameterValueField = $x("//input[@placeholder='Value']"); - protected SelenideElement validationCustomParameterValueMsg = $x("//p[contains(text(),'Value is required')]"); - protected String ddlElementLocator = "//li[@value='%s']"; - - @Step - public TopicCreateEditForm waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - nameField.shouldBe(Condition.visible); - return this; - } - - public boolean isCreateTopicButtonEnabled() { - return isEnabled(submitBtn); - } - - public boolean isDeleteCustomParameterButtonEnabled() { - return isEnabled(deleteCustomParameterBtn); - } - - @Step - public TopicCreateEditForm setTopicName(String topicName) { - nameField.shouldBe(Condition.enabled).clear(); - if (topicName != null) { - nameField.sendKeys(topicName); - } - return this; - } - - @Step - public TopicCreateEditForm setMinInsyncReplicas(Integer minInsyncReplicas) { - minInSyncReplicasField.setValue(minInsyncReplicas.toString()); - return this; - } - - @Step - public TopicCreateEditForm setTimeToRetainDataInMs(Long ms) { - timeToRetainField.setValue(ms.toString()); - return this; - } - - @Step - public TopicCreateEditForm setTimeToRetainDataInMs(String ms) { - timeToRetainField.setValue(ms); - return this; - } - - @Step - public TopicCreateEditForm setMaxSizeOnDiskInGB(MaxSizeOnDisk MaxSizeOnDisk) { - maxSizeOnDiscDdl.shouldBe(Condition.visible).click(); - $x(String.format(ddlElementLocator, MaxSizeOnDisk.getOptionValue())).shouldBe(Condition.visible).click(); - return this; - } - - @Step - public TopicCreateEditForm clickAddCustomParameterTypeButton() { - addCustomParameterTypeBtn.click(); - return this; - } - - @Step - public TopicCreateEditForm setCustomParameterType(CustomParameterType customParameterType) { - customParameterDdl.shouldBe(Condition.visible).click(); - $x(String.format(ddlElementLocator, customParameterType.getOptionValue())).shouldBe(Condition.visible).click(); - return this; - } - - @Step - public TopicCreateEditForm clearCustomParameterValue() { - clearByKeyboard(customParameterValueField); - return this; - } - - @Step - public TopicCreateEditForm setMaxMessageBytes(Long bytes) { - maxMessageBytesField.setValue(bytes.toString()); - return this; - } - - @Step - public TopicCreateEditForm setMaxMessageBytes(String bytes) { - return setMaxMessageBytes(Long.parseLong(bytes)); - } - - @Step - public TopicCreateEditForm setNumberOfPartitions(int partitions) { - partitionsField.shouldBe(Condition.enabled).clear(); - partitionsField.sendKeys(String.valueOf(partitions)); - return this; - } - - @Step - public TopicCreateEditForm setTimeToRetainDataInMsUsingButtons(String value) { - timeToRetainField - .parent() - .parent() - .$$("button") - .find(Condition.exactText(value)) - .click(); - return this; - } - - @Step - public TopicCreateEditForm selectCleanupPolicy(CleanupPolicyValue cleanupPolicyOptionValue) { - cleanUpPolicyDdl.shouldBe(Condition.visible).click(); - $x(String.format(ddlElementLocator, cleanupPolicyOptionValue.getOptionValue())).shouldBe(Condition.visible).click(); - return this; - } - - @Step - public TopicCreateEditForm selectRetentionBytes(String visibleValue) { - return selectFromDropDownByVisibleText("retentionBytes", visibleValue); - } - - @Step - public TopicCreateEditForm selectRetentionBytes(Long optionValue) { - return selectFromDropDownByOptionValue("retentionBytes", optionValue.toString()); - } - - @Step - public TopicCreateEditForm clickCreateTopicBtn() { - clickSubmitBtn(); - return this; - } - - @Step - public TopicCreateEditForm addCustomParameter(String customParameterName, - String customParameterValue) { - ElementsCollection customParametersElements = - $$("ul[role=listbox][name^=customParams][name$=name]"); - KafkaUISelectElement kafkaUISelectElement = null; - if (customParametersElements.size() == 1) { - if ("Select".equals(customParametersElements.first().getText())) { - kafkaUISelectElement = new KafkaUISelectElement(customParametersElements.first()); - } - } else { - $$("button") - .find(Condition.exactText("Add Custom Parameter")) - .click(); - customParametersElements = $$("ul[role=listbox][name^=customParams][name$=name]"); - kafkaUISelectElement = new KafkaUISelectElement(customParametersElements.last()); - } - if (kafkaUISelectElement != null) { - kafkaUISelectElement.selectByVisibleText(customParameterName); - } - $(String.format("input[name=\"customParams.%d.value\"]", customParametersElements.size() - 1)) - .setValue(customParameterValue); - return this; - } - - @Step - public TopicCreateEditForm updateCustomParameter(String customParameterName, - String customParameterValue) { - SelenideElement selenideElement = $$("ul[role=listbox][name^=customParams][name$=name]") - .find(Condition.exactText(customParameterName)); - String name = selenideElement.getAttribute("name"); - if (name != null) { - name = name.substring(0, name.lastIndexOf(".")); - } - $(String.format("input[name^=%s]", name)).setValue(customParameterValue); - return this; - } - - @Step - public TopicCreateEditForm cleanupPolicyIs(String value) { - String cleanupPolicy = new KafkaUISelectElement("cleanupPolicy") - .getCurrentValue(); - assertThat(cleanupPolicy) - .as("Clear policy value should be " + value) - .isEqualToIgnoringCase(value); - return this; - } - - @Step - public TopicCreateEditForm timeToRetainIs(String time) { - String value = timeToRetainField.getValue(); - assertThat(value) - .as("Time to retain data (in ms) should be " + time) - .isEqualTo(time); - return this; - } - - @Step - public String getCleanupPolicy() { - return new KafkaUISelectElement("cleanupPolicy").getCurrentValue(); - } - - @Step - public String getTimeToRetain() { - return timeToRetainField.getValue(); - } - - @Step - public String getMaxSizeOnDisk() { - return new KafkaUISelectElement("retentionBytes").getCurrentValue(); - } - - @Step - public String getMaxMessageBytes() { - return maxMessageBytesField.getValue(); - } - - @Step - public boolean isValidationMessageCustomParameterValueVisible() { - return isVisible(validationCustomParameterValueMsg); - } - - @Step - public String getCustomParameterValue() { - return customParameterValueField.getValue(); - } - - private static class KafkaUISelectElement { - - private final SelenideElement selectElement; - - public KafkaUISelectElement(String selectElementName) { - this.selectElement = $("ul[role=listbox][name=" + selectElementName + "]"); - } - - public KafkaUISelectElement(SelenideElement selectElement) { - this.selectElement = selectElement; - } - - public void selectByOptionValue(String optionValue) { - selectElement.click(); - selectElement - .$$x(".//ul/li[@role='option']") - .find(Condition.attribute("value", optionValue)) - .click(ClickOptions.usingJavaScript()); - } - - public void selectByVisibleText(String visibleText) { - selectElement.click(); - selectElement - .$$("ul>li[role=option]") - .find(Condition.exactText(visibleText)) - .click(); - } - - public String getCurrentValue() { - return selectElement.$("li").getText(); - } - } - - private TopicCreateEditForm selectFromDropDownByOptionValue(String dropDownElementName, - String optionValue) { - KafkaUISelectElement select = new KafkaUISelectElement(dropDownElementName); - select.selectByOptionValue(optionValue); - return this; - } - - private TopicCreateEditForm selectFromDropDownByVisibleText(String dropDownElementName, - String visibleText) { - KafkaUISelectElement select = new KafkaUISelectElement(dropDownElementName); - select.selectByVisibleText(visibleText); - return this; - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicDetails.java deleted file mode 100644 index 1de0478abe..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicDetails.java +++ /dev/null @@ -1,453 +0,0 @@ -package com.provectus.kafka.ui.pages.topic; - -import static com.codeborne.selenide.Selenide.$; -import static com.codeborne.selenide.Selenide.$$x; -import static com.codeborne.selenide.Selenide.$x; -import static com.codeborne.selenide.Selenide.sleep; -import static org.apache.commons.lang.math.RandomUtils.nextInt; - -import com.codeborne.selenide.CollectionCondition; -import com.codeborne.selenide.Condition; -import com.codeborne.selenide.ElementsCollection; -import com.codeborne.selenide.SelenideElement; -import com.provectus.kafka.ui.pages.BasePage; -import io.qameta.allure.Step; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.YearMonth; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.Objects; -import org.openqa.selenium.By; - -public class TopicDetails extends BasePage { - - protected SelenideElement clearMessagesBtn = $x(("//div[contains(text(), 'Clear messages')]")); - protected SelenideElement recreateTopicBtn = $x("//div[text()='Recreate Topic']"); - protected SelenideElement messageAmountCell = $x("//tbody/tr/td[5]"); - protected SelenideElement overviewTab = $x("//a[contains(text(),'Overview')]"); - protected SelenideElement messagesTab = $x("//a[contains(text(),'Messages')]"); - protected SelenideElement seekTypeDdl = $x("//ul[@id='selectSeekType']/li"); - protected SelenideElement seekTypeField = $x("//label[text()='Seek Type']//..//div/input"); - protected SelenideElement addFiltersBtn = $x("//button[text()='Add Filters']"); - protected SelenideElement savedFiltersLink = $x("//div[text()='Saved Filters']"); - protected SelenideElement addFilterCodeModalTitle = $x("//label[text()='Filter code']"); - protected SelenideElement addFilterCodeInput = $x("//div[@id='ace-editor']//textarea"); - protected SelenideElement saveThisFilterCheckBoxAddFilterMdl = $x("//input[@name='saveFilter']"); - protected SelenideElement displayNameInputAddFilterMdl = $x("//input[@placeholder='Enter Name']"); - protected SelenideElement cancelBtnAddFilterMdl = $x("//button[text()='Cancel']"); - protected SelenideElement addFilterBtnAddFilterMdl = $x("//button[text()='Add filter']"); - protected SelenideElement addFiltersBtnMessages = $x("//button[text()='Add Filters']"); - protected SelenideElement selectFilterBtnAddFilterMdl = $x("//button[text()='Select filter']"); - protected SelenideElement editSettingsMenu = $x("//li[@role][contains(text(),'Edit settings')]"); - protected SelenideElement removeTopicBtn = $x("//ul[@role='menu']//div[contains(text(),'Remove Topic')]"); - protected SelenideElement produceMessageBtn = $x("//div//button[text()='Produce Message']"); - protected SelenideElement contentMessageTab = $x("//html//div[@id='root']/div/main//table//p"); - protected SelenideElement cleanUpPolicyField = $x("//div[contains(text(),'Clean Up Policy')]/../span/*"); - protected SelenideElement partitionsField = $x("//div[contains(text(),'Partitions')]/../span"); - protected SelenideElement backToCreateFiltersLink = $x("//div[text()='Back To create filters']"); - protected SelenideElement confirmationMdl = $x("//div[text()= 'Confirm the action']/.."); - protected ElementsCollection messageGridItems = $$x("//tbody//tr"); - protected SelenideElement actualCalendarDate = $x("//div[@class='react-datepicker__current-month']"); - protected SelenideElement previousMonthButton = $x("//button[@aria-label='Previous Month']"); - protected SelenideElement nextMonthButton = $x("//button[@aria-label='Next Month']"); - protected SelenideElement calendarTimeFld = $x("//input[@placeholder='Time']"); - protected String dayCellLtr = "//div[@role='option'][contains(text(),'%d')]"; - protected String seekFilterDdlLocator = "//ul[@id='selectSeekType']/ul/li[text()='%s']"; - protected String savedFilterNameLocator = "//div[@role='savedFilter']/div[contains(text(),'%s')]"; - protected String consumerIdLocator = "//a[@title='%s']"; - protected String topicHeaderLocator = "//h1[contains(text(),'%s')]"; - protected String activeFilterNameLocator = "//div[@data-testid='activeSmartFilter'][contains(text(),'%s')]"; - protected String settingsGridValueLocator = "//tbody/tr/td/span[text()='%s']//ancestor::tr/td[2]/span"; - - @Step - public TopicDetails waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - overviewTab.shouldBe(Condition.visible); - return this; - } - - @Step - public TopicDetails openDetailsTab(TopicMenu menu) { - $(By.linkText(menu.toString())).shouldBe(Condition.visible).click(); - waitUntilSpinnerDisappear(); - return this; - } - - @Step - public String getSettingsGridValueByKey(String key){ - return $x(String.format(settingsGridValueLocator, key)).scrollTo().shouldBe(Condition.visible).getText(); - } - - @Step - public TopicDetails openDotMenu() { - clickByJavaScript(dotMenuBtn); - return this; - } - - @Step - public boolean isAlertWithMessageVisible(AlertHeader header, String message){ - return isAlertVisible(header, message); - } - - @Step - public TopicDetails clickEditSettingsMenu() { - editSettingsMenu.shouldBe(Condition.visible).click(); - return this; - } - - @Step - public boolean isConfirmationMdlVisible(){ - return isVisible(confirmationMdl); - } - - @Step - public TopicDetails clickClearMessagesMenu() { - clearMessagesBtn.shouldBe(Condition.visible).click(); - return this; - } - - @Step - public TopicDetails clickRecreateTopicMenu(){ - recreateTopicBtn.shouldBe(Condition.visible).click(); - return this; - } - - @Step - public String getCleanUpPolicy() { - return cleanUpPolicyField.getText(); - } - - @Step - public int getPartitions() { - return Integer.parseInt(partitionsField.getText().trim()); - } - - @Step - public boolean isTopicHeaderVisible(String topicName) { - return isVisible($x(String.format(topicHeaderLocator, topicName))); - } - - @Step - public TopicDetails clickDeleteTopicMenu() { - removeTopicBtn.shouldBe(Condition.visible).click(); - return this; - } - - @Step - public TopicDetails clickConfirmBtnMdl() { - clickConfirmButton(); - return this; - } - - @Step - public TopicDetails clickProduceMessageBtn() { - clickByJavaScript(produceMessageBtn); - return this; - } - - @Step - public TopicDetails selectSeekTypeDdlMessagesTab(String seekTypeName){ - seekTypeDdl.shouldBe(Condition.enabled).click(); - $x(String.format(seekFilterDdlLocator, seekTypeName)).shouldBe(Condition.visible).click(); - return this; - } - - @Step - public TopicDetails setSeekTypeValueFldMessagesTab(String seekTypeValue){ - seekTypeField.shouldBe(Condition.enabled).sendKeys(seekTypeValue); - return this; - } - - @Step - public TopicDetails clickSubmitFiltersBtnMessagesTab(){ - clickByJavaScript(submitBtn); - waitUntilSpinnerDisappear(); - return this; - } - - @Step - public TopicDetails clickMessagesAddFiltersBtn() { - addFiltersBtn.shouldBe(Condition.enabled).click(); - return this; - } - - @Step - public TopicDetails openSavedFiltersListMdl(){ - savedFiltersLink.shouldBe(Condition.enabled).click(); - backToCreateFiltersLink.shouldBe(Condition.visible); - return this; - } - - @Step - public boolean isFilterVisibleAtSavedFiltersMdl(String filterName){ - return isVisible($x(String.format(savedFilterNameLocator,filterName))); - } - - @Step - public TopicDetails selectFilterAtSavedFiltersMdl(String filterName){ - $x(String.format(savedFilterNameLocator, filterName)).shouldBe(Condition.enabled).click(); - return this; - } - - @Step - public TopicDetails clickSelectFilterBtnAtSavedFiltersMdl(){ - selectFilterBtnAddFilterMdl.shouldBe(Condition.enabled).click(); - addFilterCodeModalTitle.shouldBe(Condition.disappear); - return this; - } - - @Step - public TopicDetails waitUntilAddFiltersMdlVisible() { - addFilterCodeModalTitle.shouldBe(Condition.visible); - return this; - } - - @Step - public TopicDetails setFilterCodeFieldAddFilterMdl(String filterCode) { - addFilterCodeInput.shouldBe(Condition.enabled).sendKeys(filterCode); - return this; - } - - @Step - public TopicDetails selectSaveThisFilterCheckboxMdl(boolean select){ - selectElement(saveThisFilterCheckBoxAddFilterMdl, select); - return this; - } - - @Step - public boolean isSaveThisFilterCheckBoxSelected() { - return isSelected(saveThisFilterCheckBoxAddFilterMdl); - } - - @Step - public TopicDetails setDisplayNameFldAddFilterMdl(String displayName) { - displayNameInputAddFilterMdl.shouldBe(Condition.enabled).sendKeys(displayName); - return this; - } - - @Step - public TopicDetails clickAddFilterBtnAndCloseMdl(boolean closeModal) { - addFilterBtnAddFilterMdl.shouldBe(Condition.enabled).click(); - if(closeModal){ - addFilterCodeModalTitle.shouldBe(Condition.hidden);} - else{ - addFilterCodeModalTitle.shouldBe(Condition.visible); - } - return this; - } - - @Step - public boolean isAddFilterBtnAddFilterMdlEnabled() { - return isEnabled(addFilterBtnAddFilterMdl); - } - - @Step - public boolean isActiveFilterVisible(String activeFilterName) { - return isVisible($x(String.format(activeFilterNameLocator, activeFilterName))); - } - - public List getAllAddFilterModalVisibleElements() { - return Arrays.asList(savedFiltersLink, displayNameInputAddFilterMdl, addFilterBtnAddFilterMdl, cancelBtnAddFilterMdl); - } - - public List getAllAddFilterModalEnabledElements() { - return Arrays.asList(displayNameInputAddFilterMdl, cancelBtnAddFilterMdl); - } - - public List getAllAddFilterModalDisabledElements() { - return Arrays.asList(addFilterBtnAddFilterMdl); - } - - @Step - public TopicDetails openConsumerGroup(String consumerId) { - $x(String.format(consumerIdLocator, consumerId)).click(); - return this; - } - - @Step - public boolean isKeyMessageVisible(String keyMessage) { - return keyMessage.equals($("td[title]").getText()); - } - - @Step - public boolean isContentMessageVisible(String contentMessage) { - return contentMessage.matches(contentMessageTab.getText().trim()); - } - - private void selectYear(int expectedYear) { - while (getActualCalendarDate().getYear() > expectedYear) { - clickByJavaScript(previousMonthButton); - sleep(1000); - if (LocalTime.now().plusMinutes(3).isBefore(LocalTime.now())) { - throw new IllegalArgumentException("Unable to select year"); - } - } - } - - private void selectMonth(int expectedMonth) { - while (getActualCalendarDate().getMonthValue() > expectedMonth) { - clickByJavaScript(previousMonthButton); - sleep(1000); - if (LocalTime.now().plusMinutes(3).isBefore(LocalTime.now())) { - throw new IllegalArgumentException("Unable to select month"); - } - } - } - - private void selectDay(int expectedDay) { - Objects.requireNonNull($$x(String.format(dayCellLtr, expectedDay)).stream() - .filter(day -> !Objects.requireNonNull(day.getAttribute("class")).contains("outside-month")) - .findFirst().orElse(null)).shouldBe(Condition.enabled).click(); - } - - private void setTime(LocalDateTime dateTime) { - calendarTimeFld.shouldBe(Condition.enabled) - .sendKeys(String.valueOf(dateTime.getHour()), String.valueOf(dateTime.getMinute())); - } - - @Step - public TopicDetails selectDateAndTimeByCalendar(LocalDateTime dateTime) { - setTime(dateTime); - selectYear(dateTime.getYear()); - selectMonth(dateTime.getMonthValue()); - selectDay(dateTime.getDayOfMonth()); - return this; - } - - private LocalDate getActualCalendarDate() { - String monthAndYearStr = actualCalendarDate.getText().trim(); - DateTimeFormatter formatter = new DateTimeFormatterBuilder() - .parseCaseInsensitive() - .append(DateTimeFormatter.ofPattern("MMMM yyyy")) - .toFormatter(Locale.ENGLISH); - YearMonth yearMonth = formatter.parse(monthAndYearStr, YearMonth::from); - return yearMonth.atDay(1); - } - - @Step - public TopicDetails openCalendarSeekType(){ - seekTypeField.shouldBe(Condition.enabled).click(); - actualCalendarDate.shouldBe(Condition.visible); - return this; - } - - @Step - public int getMessageCountAmount() { - return Integer.parseInt(messageAmountCell.getText().trim()); - } - - private List initItems() { - List gridItemList = new ArrayList<>(); - allGridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) - .forEach(item -> gridItemList.add(new TopicDetails.MessageGridItem(item))); - return gridItemList; - } - - @Step - public TopicDetails.MessageGridItem getMessageByOffset(int offset) { - return initItems().stream() - .filter(e -> e.getOffset() == offset) - .findFirst().orElse(null); - } - - @Step - public List getAllMessages(){ - return initItems(); - } - - @Step - public TopicDetails.MessageGridItem getRandomMessage() { - return getMessageByOffset(nextInt(initItems().size() - 1)); - } - - public enum TopicMenu { - OVERVIEW("Overview"), - MESSAGES("Messages"), - CONSUMERS("Consumers"), - SETTINGS("Settings"); - - private final String value; - - TopicMenu(String value) { - this.value = value; - } - - public String toString() { - return value; - } - } - - public static class MessageGridItem extends BasePage { - - private final SelenideElement element; - - private MessageGridItem(SelenideElement element) { - this.element = element; - } - - @Step - public MessageGridItem clickExpand() { - clickByJavaScript(element.$x("./td[1]/span")); - return this; - } - - private SelenideElement getOffsetElm() { - return element.$x("./td[2]"); - } - - @Step - public int getOffset() { - return Integer.parseInt(getOffsetElm().getText().trim()); - } - - @Step - public int getPartition() { - return Integer.parseInt(element.$x("./td[3]").getText().trim()); - } - - @Step - public LocalDateTime getTimestamp() { - String timestampValue = element.$x("./td[4]/div").getText().trim(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M/d/yyyy, HH:mm:ss"); - return LocalDateTime.parse(timestampValue, formatter); - } - - @Step - public String getKey() { - return element.$x("./td[5]").getText().trim(); - } - - @Step - public String getValue() { - return element.$x("./td[6]/span/p").getText().trim(); - } - - @Step - public MessageGridItem openDotMenu() { - getOffsetElm().hover(); - element.$x("./td[7]/div/button[@aria-label='Dropdown Toggle']") - .shouldBe(Condition.visible).click(); - return this; - } - - @Step - public MessageGridItem clickCopyToClipBoard() { - clickByJavaScript(element.$x("./td[7]//li[text() = 'Copy to clipboard']") - .shouldBe(Condition.visible)); - return this; - } - - @Step - public MessageGridItem clickSaveAsFile() { - clickByJavaScript(element.$x("./td[7]//li[text() = 'Save as a file']") - .shouldBe(Condition.visible)); - return this; - } - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicSettingsTab.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicSettingsTab.java deleted file mode 100644 index 4bf78fb2af..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicSettingsTab.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.provectus.kafka.ui.pages.topic; - -import static com.codeborne.selenide.Selenide.$x; - -import com.codeborne.selenide.CollectionCondition; -import com.codeborne.selenide.Condition; -import com.codeborne.selenide.SelenideElement; -import com.provectus.kafka.ui.pages.BasePage; -import io.qameta.allure.Step; -import java.util.ArrayList; -import java.util.List; - -public class TopicSettingsTab extends BasePage { - - protected SelenideElement defaultValueColumnHeaderLocator = $x("//div[text() = 'Default Value']"); - - @Step - public TopicSettingsTab waitUntilScreenReady(){ - waitUntilSpinnerDisappear(); - defaultValueColumnHeaderLocator.shouldBe(Condition.visible); - return this; - } - - private List initGridItems() { - List gridItemList = new ArrayList<>(); - allGridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) - .forEach(item -> gridItemList.add(new SettingsGridItem(item))); - return gridItemList; - } - - private TopicSettingsTab.SettingsGridItem getItemByKey(String key){ - return initGridItems().stream() - .filter(e ->e.getKey().equals(key)) - .findFirst().orElse(null); - } - - @Step - public String getValueByKey(String key){ - return getItemByKey(key).getValue(); - } - - public static class SettingsGridItem extends BasePage { - - private final SelenideElement element; - - public SettingsGridItem(SelenideElement element) { - this.element = element; - } - - @Step - public String getKey(){ - return element.$x("./td[1]/span").getText().trim(); - } - - @Step - public String getValue(){ - return element.$x("./td[2]/span").getText().trim(); - } - - @Step - public String getDefaultValue() { - return element.$x("./td[3]/span").getText().trim(); - } - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicsList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicsList.java deleted file mode 100644 index 9db1d18c8b..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicsList.java +++ /dev/null @@ -1,243 +0,0 @@ -package com.provectus.kafka.ui.pages.topic; - -import static com.codeborne.selenide.Condition.visible; -import static com.codeborne.selenide.Selenide.$x; - -import com.codeborne.selenide.CollectionCondition; -import com.codeborne.selenide.Condition; -import com.codeborne.selenide.SelenideElement; -import com.provectus.kafka.ui.pages.BasePage; -import io.qameta.allure.Step; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class TopicsList extends BasePage { - - protected SelenideElement topicListHeader = $x("//h1[text()='Topics']"); - protected SelenideElement addTopicBtn = $x("//button[normalize-space(text()) ='Add a Topic']"); - protected SelenideElement searchField = $x("//input[@placeholder='Search by Topic Name']"); - protected SelenideElement showInternalRadioBtn = $x("//input[@name='ShowInternalTopics']"); - protected SelenideElement deleteSelectedTopicsBtn = $x("//button[text()='Delete selected topics']"); - protected SelenideElement copySelectedTopicBtn = $x("//button[text()='Copy selected topic']"); - protected SelenideElement purgeMessagesOfSelectedTopicsBtn = $x("//button[text()='Purge messages of selected topics']"); - protected SelenideElement clearMessagesBtn = $x("//ul[contains(@class ,'open')]//div[text()='Clear Messages']"); - protected SelenideElement recreateTopicBtn = $x("//ul[contains(@class ,'open')]//div[text()='Recreate Topic']"); - protected SelenideElement removeTopicBtn = $x("//ul[contains(@class ,'open')]//div[text()='Remove Topic']"); - - @Step - public TopicsList waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - topicListHeader.shouldBe(visible); - return this; - } - - @Step - public TopicsList clickAddTopicBtn() { - clickByJavaScript(addTopicBtn); - return this; - } - - @Step - public boolean isTopicVisible(String topicName) { - tableGrid.shouldBe(visible); - return isVisible(getTableElement(topicName)); - } - - @Step - public boolean isShowInternalRadioBtnSelected() { - return isSelected(showInternalRadioBtn); - } - - @Step - public TopicsList setShowInternalRadioButton(boolean select) { - selectElement(showInternalRadioBtn, select); - return this; - } - - @Step - public TopicsList openTopic(String topicName) { - getTopicItem(topicName).openItem(); - return this; - } - - @Step - public TopicsList openDotMenuByTopicName(String topicName){ - getTopicItem(topicName).openDotMenu(); - return this; - } - - @Step - public boolean isCopySelectedTopicBtnEnabled(){ - return isEnabled(copySelectedTopicBtn); - } - - @Step - public List getActionButtons() { - return Stream.of(deleteSelectedTopicsBtn, copySelectedTopicBtn, purgeMessagesOfSelectedTopicsBtn) - .collect(Collectors.toList()); - } - - @Step - public TopicsList clickCopySelectedTopicBtn(){ - copySelectedTopicBtn.shouldBe(Condition.enabled).click(); - return this; - } - - @Step - public TopicsList clickClearMessagesBtn(){ - clickByJavaScript(clearMessagesBtn.shouldBe(visible)); - return this; - } - - @Step - public TopicsList clickRecreateTopicBtn(){ - clickByJavaScript(recreateTopicBtn.shouldBe(visible)); - return this; - } - - @Step - public TopicsList clickRemoveTopicBtn(){ - clickByJavaScript(removeTopicBtn.shouldBe(visible)); - return this; - } - - @Step - public TopicsList clickConfirmBtnMdl() { - clickConfirmButton(); - return this; - } - - @Step - public boolean isAlertWithMessageVisible(AlertHeader header, String message) { - return isAlertVisible(header, message); - } - - private List getVisibleColumnHeaders() { - return Stream.of("Replication Factor","Number of messages","Topic Name", "Partitions", "Out of sync replicas", "Size") - .map(name -> $x(String.format(columnHeaderLocator, name))) - .collect(Collectors.toList()); - } - - private List getEnabledColumnHeaders(){ - return Stream.of("Topic Name", "Partitions", "Out of sync replicas", "Size") - .map(name -> $x(String.format(columnHeaderLocator, name))) - .collect(Collectors.toList()); - } - - @Step - public List getAllVisibleElements() { - List visibleElements = new ArrayList<>(getVisibleColumnHeaders()); - visibleElements.addAll(Arrays.asList(searchField, addTopicBtn, tableGrid)); - visibleElements.addAll(getActionButtons()); - return visibleElements; - } - - @Step - public List getAllEnabledElements() { - List enabledElements = new ArrayList<>(getEnabledColumnHeaders()); - enabledElements.addAll(Arrays.asList(searchField, showInternalRadioBtn,addTopicBtn)); - return enabledElements; - } - - private List initGridItems() { - List gridItemList = new ArrayList<>(); - allGridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) - .forEach(item -> gridItemList.add(new TopicGridItem(item))); - return gridItemList; - } - - @Step - public TopicGridItem getTopicItem(String name) { - return initGridItems().stream() - .filter(e -> e.getName().equals(name)) - .findFirst().orElse(null); - } - - @Step - public List getNonInternalTopics() { - return initGridItems().stream() - .filter(e -> !e.isInternal()) - .collect(Collectors.toList()); - } - - @Step - public List getInternalTopics() { - return initGridItems().stream() - .filter(TopicGridItem::isInternal) - .collect(Collectors.toList()); - } - - public static class TopicGridItem extends BasePage { - - private final SelenideElement element; - - public TopicGridItem(SelenideElement element) { - this.element = element; - } - - @Step - public TopicsList selectItem(boolean select) { - selectElement(element.$x("./td[1]/input"), select); - return new TopicsList(); - } - - @Step - public boolean isInternal() { - boolean internal = false; - try { - element.$x("./td[2]/a/span").shouldBe(visible, Duration.ofMillis(500)); - internal = true; - } catch (Throwable ignored) { - } - return internal; - } - - private SelenideElement getNameElm() { - return element.$x("./td[2]"); - } - - @Step - public String getName() { - return getNameElm().getText().trim(); - } - - @Step - public void openItem() { - getNameElm().click(); - } - - @Step - public int getPartition() { - return Integer.parseInt(element.$x("./td[3]").getText().trim()); - } - - @Step - public int getOutOfSyncReplicas() { - return Integer.parseInt(element.$x("./td[4]").getText().trim()); - } - - @Step - public int getReplicationFactor() { - return Integer.parseInt(element.$x("./td[5]").getText().trim()); - } - - @Step - public int getNumberOfMessages() { - return Integer.parseInt(element.$x("./td[6]").getText().trim()); - } - - @Step - public int getSize() { - return Integer.parseInt(element.$x("./td[7]").getText().trim()); - } - - @Step - public void openDotMenu(){ - element.$x("./td[8]//button").click(); - } - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/enums/CleanupPolicyValue.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/enums/CleanupPolicyValue.java deleted file mode 100644 index 86b87325a0..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/enums/CleanupPolicyValue.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.provectus.kafka.ui.pages.topic.enums; - -public enum CleanupPolicyValue { - DELETE("delete", "Delete"), - COMPACT("compact", "Compact"), - COMPACT_DELETE("compact,delete", "Compact,Delete"); - - private final String optionValue; - private final String visibleText; - - CleanupPolicyValue(String optionValue, String visibleText) { - this.optionValue = optionValue; - this.visibleText = visibleText; - } - - public String getOptionValue() { - return optionValue; - } - - public String getVisibleText() { - return visibleText; - } -} - diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/enums/CustomParameterType.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/enums/CustomParameterType.java deleted file mode 100644 index bc2f3befae..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/enums/CustomParameterType.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.provectus.kafka.ui.pages.topic.enums; - -public enum CustomParameterType { - COMPRESSION_TYPE("compression.type"), - DELETE_RETENTION_MS("delete.retention.ms"), - FILE_DELETE_DELAY_MS("file.delete.delay.ms"), - FLUSH_MESSAGES("flush.messages"), - FLUSH_MS("flush.ms"), - FOLLOWER_REPLICATION_THROTTLED_REPLICAS("follower.replication.throttled.replicas"), - INDEX_INTERVAL_BYTES("index.interval.bytes"), - LEADER_REPLICATION_THROTTLED_REPLICAS("leader.replication.throttled.replicas"), - MAX_COMPACTION_LAG_MS("max.compaction.lag.ms"), - MESSAGE_DOWNCONVERSION_ENABLE("message.downconversion.enable"), - MESSAGE_FORMAT_VERSION("message.format.version"), - MESSAGE_TIMESTAMP_DIFFERENCE_MAX_MS("message.timestamp.difference.max.ms"), - MESSAGE_TIMESTAMP_TYPE("message.timestamp.type"), - MIN_CLEANABLE_DIRTY_RATIO("min.cleanable.dirty.ratio"), - MIN_COMPACTION_LAG_MS("min.compaction.lag.ms"), - PREALLOCATE("preallocate"), - RETENTION_BYTES("retention.bytes"), - SEGMENT_BYTES("segment.bytes"), - SEGMENT_INDEX_BYTES("segment.index.bytes"), - SEGMENT_JITTER_MS("segment.jitter.ms"), - SEGMENT_MS("segment.ms"), - UNCLEAN_LEADER_ELECTION_ENABLE("unclean.leader.election.enable"); - - private final String optionValue; - - CustomParameterType(String optionValue) { - this.optionValue = optionValue; - } - - public String getOptionValue() { - return optionValue; - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/enums/MaxSizeOnDisk.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/enums/MaxSizeOnDisk.java deleted file mode 100644 index bdb476d1e6..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/enums/MaxSizeOnDisk.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.provectus.kafka.ui.pages.topic.enums; - -public enum MaxSizeOnDisk { - NOT_SET("-1", "Not Set"), - SIZE_1_GB("1073741824", "1 GB"), - SIZE_10_GB("10737418240", "10 GB"), - SIZE_20_GB("21474836480", "20 GB"), - SIZE_50_GB("53687091200", "50 GB"); - - private final String optionValue; - private final String visibleText; - - MaxSizeOnDisk(String optionValue, String visibleText) { - this.optionValue = optionValue; - this.visibleText = visibleText; - } - - public String getOptionValue() { - return optionValue; - } - - public String getVisibleText() { - return visibleText; - } -} - diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/ProduceMessagePanel.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java similarity index 94% rename from kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/ProduceMessagePanel.java rename to kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java index d4dd3d8c15..a16d2c83a0 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/ProduceMessagePanel.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java @@ -1,14 +1,15 @@ -package com.provectus.kafka.ui.pages.topic; - -import static com.codeborne.selenide.Selenide.$x; -import static com.codeborne.selenide.Selenide.refresh; +package com.provectus.kafka.ui.pages.topics; import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; + import java.util.Arrays; +import static com.codeborne.selenide.Selenide.$x; +import static com.codeborne.selenide.Selenide.refresh; + public class ProduceMessagePanel extends BasePage { protected SelenideElement keyTextArea = $x("//div[@id='key']/textarea"); @@ -20,7 +21,7 @@ public class ProduceMessagePanel extends BasePage { protected SelenideElement contentSerdeDdl = $x("//ul[@name='valueSerde']"); @Step - public ProduceMessagePanel waitUntilScreenReady(){ + public ProduceMessagePanel waitUntilScreenReady() { waitUntilSpinnerDisappear(); Arrays.asList(partitionDdl, keySerdeDdl, contentSerdeDdl).forEach(element -> element.shouldBe(Condition.visible)); return this; diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicCreateEditForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicCreateEditForm.java new file mode 100644 index 0000000000..f60bd6d431 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicCreateEditForm.java @@ -0,0 +1,276 @@ +package com.provectus.kafka.ui.pages.topics; + +import com.codeborne.selenide.*; +import com.provectus.kafka.ui.pages.BasePage; +import com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue; +import com.provectus.kafka.ui.pages.topics.enums.CustomParameterType; +import com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk; +import com.provectus.kafka.ui.pages.topics.enums.TimeToRetain; +import io.qameta.allure.Step; + +import static com.codeborne.selenide.Selenide.*; + +public class TopicCreateEditForm extends BasePage { + + protected SelenideElement timeToRetainField = $x("//input[@id='timeToRetain']"); + protected SelenideElement partitionsField = $x("//input[@name='partitions']"); + protected SelenideElement nameField = $x("//input[@name='name']"); + protected SelenideElement maxMessageBytesField = $x("//input[@name='maxMessageBytes']"); + protected SelenideElement minInSyncReplicasField = $x("//input[@name='minInSyncReplicas']"); + protected SelenideElement cleanUpPolicyDdl = $x("//ul[@id='topicFormCleanupPolicy']"); + protected SelenideElement maxSizeOnDiscDdl = $x("//ul[@id='topicFormRetentionBytes']"); + protected SelenideElement customParameterDdl = $x("//ul[contains(@name,'customParams')]"); + protected SelenideElement deleteCustomParameterBtn = $x("//span[contains(@title,'Delete customParam')]"); + protected SelenideElement addCustomParameterTypeBtn = $x("//button[contains(text(),'Add Custom Parameter')]"); + protected SelenideElement customParameterValueField = $x("//input[@placeholder='Value']"); + protected SelenideElement validationCustomParameterValueMsg = $x("//p[contains(text(),'Value is required')]"); + protected String ddlElementLocator = "//li[@value='%s']"; + protected String btnTimeToRetainLocator = "//button[@class][text()='%s']"; + + + @Step + public TopicCreateEditForm waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + nameField.shouldBe(Condition.visible); + return this; + } + + public boolean isCreateTopicButtonEnabled() { + return isEnabled(submitBtn); + } + + public boolean isDeleteCustomParameterButtonEnabled() { + return isEnabled(deleteCustomParameterBtn); + } + + public boolean isNameFieldEnabled() { + return isEnabled(nameField); + } + + @Step + public TopicCreateEditForm setTopicName(String topicName) { + nameField.shouldBe(Condition.enabled).clear(); + if (topicName != null) { + nameField.sendKeys(topicName); + } + return this; + } + + @Step + public TopicCreateEditForm setMinInsyncReplicas(Integer minInsyncReplicas) { + minInSyncReplicasField.setValue(minInsyncReplicas.toString()); + return this; + } + + @Step + public TopicCreateEditForm setTimeToRetainDataInMs(Long ms) { + timeToRetainField.setValue(ms.toString()); + return this; + } + + @Step + public TopicCreateEditForm setTimeToRetainDataInMs(String ms) { + timeToRetainField.setValue(ms); + return this; + } + + @Step + public TopicCreateEditForm setMaxSizeOnDiskInGB(MaxSizeOnDisk MaxSizeOnDisk) { + maxSizeOnDiscDdl.shouldBe(Condition.visible).click(); + $x(String.format(ddlElementLocator, MaxSizeOnDisk.getOptionValue())).shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicCreateEditForm clickAddCustomParameterTypeButton() { + addCustomParameterTypeBtn.click(); + return this; + } + + @Step + public TopicCreateEditForm openCustomParameterTypeDdl() { + customParameterDdl.shouldBe(Condition.visible).click(); + ddlOptions.shouldHave(CollectionCondition.sizeGreaterThan(0)); + return this; + } + + @Step + public ElementsCollection getAllDdlOptions() { + return getDdlOptions(); + } + + @Step + public TopicCreateEditForm setCustomParameterType(CustomParameterType customParameterType) { + openCustomParameterTypeDdl(); + $x(String.format(ddlElementLocator, customParameterType.getOptionValue())).shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicCreateEditForm clearCustomParameterValue() { + clearByKeyboard(customParameterValueField); + return this; + } + + @Step + public TopicCreateEditForm setNumberOfPartitions(int partitions) { + partitionsField.shouldBe(Condition.enabled).clear(); + partitionsField.sendKeys(String.valueOf(partitions)); + return this; + } + + @Step + public TopicCreateEditForm setTimeToRetainDataByButtons(TimeToRetain timeToRetain) { + $x(String.format(btnTimeToRetainLocator, timeToRetain.getButton())).shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicCreateEditForm selectCleanupPolicy(CleanupPolicyValue cleanupPolicyOptionValue) { + cleanUpPolicyDdl.shouldBe(Condition.visible).click(); + $x(String.format(ddlElementLocator, cleanupPolicyOptionValue.getOptionValue())).shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicCreateEditForm selectRetentionBytes(String visibleValue) { + return selectFromDropDownByVisibleText("retentionBytes", visibleValue); + } + + @Step + public TopicCreateEditForm selectRetentionBytes(Long optionValue) { + return selectFromDropDownByOptionValue("retentionBytes", optionValue.toString()); + } + + @Step + public TopicCreateEditForm clickCreateTopicBtn() { + clickSubmitBtn(); + return this; + } + + @Step + public TopicCreateEditForm addCustomParameter(String customParameterName, + String customParameterValue) { + ElementsCollection customParametersElements = + $$("ul[role=listbox][name^=customParams][name$=name]"); + KafkaUISelectElement kafkaUISelectElement = null; + if (customParametersElements.size() == 1) { + if ("Select".equals(customParametersElements.first().getText())) { + kafkaUISelectElement = new KafkaUISelectElement(customParametersElements.first()); + } + } else { + $$("button") + .find(Condition.exactText("Add Custom Parameter")) + .click(); + customParametersElements = $$("ul[role=listbox][name^=customParams][name$=name]"); + kafkaUISelectElement = new KafkaUISelectElement(customParametersElements.last()); + } + if (kafkaUISelectElement != null) { + kafkaUISelectElement.selectByVisibleText(customParameterName); + } + $(String.format("input[name=\"customParams.%d.value\"]", customParametersElements.size() - 1)) + .setValue(customParameterValue); + return this; + } + + @Step + public TopicCreateEditForm updateCustomParameter(String customParameterName, + String customParameterValue) { + SelenideElement selenideElement = $$("ul[role=listbox][name^=customParams][name$=name]") + .find(Condition.exactText(customParameterName)); + String name = selenideElement.getAttribute("name"); + if (name != null) { + name = name.substring(0, name.lastIndexOf(".")); + } + $(String.format("input[name^=%s]", name)).setValue(customParameterValue); + return this; + } + + @Step + public String getCleanupPolicy() { + return new KafkaUISelectElement("cleanupPolicy").getCurrentValue(); + } + + @Step + public String getTimeToRetain() { + return timeToRetainField.getValue(); + } + + @Step + public String getMaxSizeOnDisk() { + return new KafkaUISelectElement("retentionBytes").getCurrentValue(); + } + + @Step + public String getMaxMessageBytes() { + return maxMessageBytesField.getValue(); + } + + @Step + public TopicCreateEditForm setMaxMessageBytes(Long bytes) { + maxMessageBytesField.setValue(bytes.toString()); + return this; + } + + @Step + public TopicCreateEditForm setMaxMessageBytes(String bytes) { + return setMaxMessageBytes(Long.parseLong(bytes)); + } + + @Step + public boolean isValidationMessageCustomParameterValueVisible() { + return isVisible(validationCustomParameterValueMsg); + } + + @Step + public String getCustomParameterValue() { + return customParameterValueField.getValue(); + } + + private TopicCreateEditForm selectFromDropDownByOptionValue(String dropDownElementName, + String optionValue) { + KafkaUISelectElement select = new KafkaUISelectElement(dropDownElementName); + select.selectByOptionValue(optionValue); + return this; + } + + private TopicCreateEditForm selectFromDropDownByVisibleText(String dropDownElementName, + String visibleText) { + KafkaUISelectElement select = new KafkaUISelectElement(dropDownElementName); + select.selectByVisibleText(visibleText); + return this; + } + + private static class KafkaUISelectElement { + + private final SelenideElement selectElement; + + public KafkaUISelectElement(String selectElementName) { + this.selectElement = $("ul[role=listbox][name=" + selectElementName + "]"); + } + + public KafkaUISelectElement(SelenideElement selectElement) { + this.selectElement = selectElement; + } + + public void selectByOptionValue(String optionValue) { + selectElement.click(); + selectElement + .$$x(".//ul/li[@role='option']") + .find(Condition.attribute("value", optionValue)) + .click(ClickOptions.usingJavaScript()); + } + + public void selectByVisibleText(String visibleText) { + selectElement.click(); + selectElement + .$$("ul>li[role=option]") + .find(Condition.exactText(visibleText)) + .click(); + } + + public String getCurrentValue() { + return selectElement.$("li").getText(); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java new file mode 100644 index 0000000000..a99afd903b --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java @@ -0,0 +1,463 @@ +package com.provectus.kafka.ui.pages.topics; + +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.ElementsCollection; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; +import org.openqa.selenium.By; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.*; + +import static com.codeborne.selenide.Selenide.*; +import static org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils.nextInt; + +public class TopicDetails extends BasePage { + + protected SelenideElement clearMessagesBtn = $x(("//div[contains(text(), 'Clear messages')]")); + protected SelenideElement recreateTopicBtn = $x("//div[text()='Recreate Topic']"); + protected SelenideElement messageAmountCell = $x("//tbody/tr/td[5]"); + protected SelenideElement overviewTab = $x("//a[contains(text(),'Overview')]"); + protected SelenideElement messagesTab = $x("//a[contains(text(),'Messages')]"); + protected SelenideElement seekTypeDdl = $x("//ul[@id='selectSeekType']/li"); + protected SelenideElement seekTypeField = $x("//label[text()='Seek Type']//..//div/input"); + protected SelenideElement addFiltersBtn = $x("//button[text()='Add Filters']"); + protected SelenideElement savedFiltersLink = $x("//div[text()='Saved Filters']"); + protected SelenideElement addFilterCodeModalTitle = $x("//label[text()='Filter code']"); + protected SelenideElement addFilterCodeInput = $x("//div[@id='ace-editor']//textarea"); + protected SelenideElement saveThisFilterCheckBoxAddFilterMdl = $x("//input[@name='saveFilter']"); + protected SelenideElement displayNameInputAddFilterMdl = $x("//input[@placeholder='Enter Name']"); + protected SelenideElement cancelBtnAddFilterMdl = $x("//button[text()='Cancel']"); + protected SelenideElement addFilterBtnAddFilterMdl = $x("//button[text()='Add filter']"); + protected SelenideElement addFiltersBtnMessages = $x("//button[text()='Add Filters']"); + protected SelenideElement selectFilterBtnAddFilterMdl = $x("//button[text()='Select filter']"); + protected SelenideElement editSettingsMenu = $x("//li[@role][contains(text(),'Edit settings')]"); + protected SelenideElement removeTopicBtn = $x("//ul[@role='menu']//div[contains(text(),'Remove Topic')]"); + protected SelenideElement produceMessageBtn = $x("//div//button[text()='Produce Message']"); + protected SelenideElement contentMessageTab = $x("//html//div[@id='root']/div/main//table//p"); + protected SelenideElement cleanUpPolicyField = $x("//div[contains(text(),'Clean Up Policy')]/../span/*"); + protected SelenideElement partitionsField = $x("//div[contains(text(),'Partitions')]/../span"); + protected SelenideElement backToCreateFiltersLink = $x("//div[text()='Back To create filters']"); + protected ElementsCollection messageGridItems = $$x("//tbody//tr"); + protected SelenideElement actualCalendarDate = $x("//div[@class='react-datepicker__current-month']"); + protected SelenideElement previousMonthButton = $x("//button[@aria-label='Previous Month']"); + protected SelenideElement nextMonthButton = $x("//button[@aria-label='Next Month']"); + protected SelenideElement calendarTimeFld = $x("//input[@placeholder='Time']"); + protected String dayCellLtr = "//div[@role='option'][contains(text(),'%d')]"; + protected String seekFilterDdlLocator = "//ul[@id='selectSeekType']/ul/li[text()='%s']"; + protected String savedFilterNameLocator = "//div[@role='savedFilter']/div[contains(text(),'%s')]"; + protected String consumerIdLocator = "//a[@title='%s']"; + protected String topicHeaderLocator = "//h1[contains(text(),'%s')]"; + protected String activeFilterNameLocator = "//div[@data-testid='activeSmartFilter'][contains(text(),'%s')]"; + protected String settingsGridValueLocator = "//tbody/tr/td/span[text()='%s']//ancestor::tr/td[2]/span"; + + @Step + public TopicDetails waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + overviewTab.shouldBe(Condition.visible); + return this; + } + + @Step + public TopicDetails openDetailsTab(TopicMenu menu) { + $(By.linkText(menu.toString())).shouldBe(Condition.visible).click(); + waitUntilSpinnerDisappear(); + return this; + } + + @Step + public String getSettingsGridValueByKey(String key) { + return $x(String.format(settingsGridValueLocator, key)).scrollTo().shouldBe(Condition.visible).getText(); + } + + @Step + public TopicDetails openDotMenu() { + clickByJavaScript(dotMenuBtn); + return this; + } + + @Step + public boolean isAlertWithMessageVisible(AlertHeader header, String message) { + return isAlertVisible(header, message); + } + + @Step + public TopicDetails clickEditSettingsMenu() { + editSettingsMenu.shouldBe(Condition.visible).click(); + return this; + } + + @Step + public boolean isConfirmationMdlVisible() { + return isConfirmationModalVisible(); + } + + @Step + public TopicDetails clickClearMessagesMenu() { + clearMessagesBtn.shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicDetails clickRecreateTopicMenu() { + recreateTopicBtn.shouldBe(Condition.visible).click(); + return this; + } + + @Step + public String getCleanUpPolicy() { + return cleanUpPolicyField.getText(); + } + + @Step + public int getPartitions() { + return Integer.parseInt(partitionsField.getText().trim()); + } + + @Step + public boolean isTopicHeaderVisible(String topicName) { + return isVisible($x(String.format(topicHeaderLocator, topicName))); + } + + @Step + public TopicDetails clickDeleteTopicMenu() { + removeTopicBtn.shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicDetails clickConfirmBtnMdl() { + clickConfirmButton(); + return this; + } + + @Step + public TopicDetails clickProduceMessageBtn() { + clickByJavaScript(produceMessageBtn); + return this; + } + + @Step + public TopicDetails selectSeekTypeDdlMessagesTab(String seekTypeName) { + seekTypeDdl.shouldBe(Condition.enabled).click(); + $x(String.format(seekFilterDdlLocator, seekTypeName)).shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicDetails setSeekTypeValueFldMessagesTab(String seekTypeValue) { + seekTypeField.shouldBe(Condition.enabled).sendKeys(seekTypeValue); + return this; + } + + @Step + public TopicDetails clickSubmitFiltersBtnMessagesTab() { + clickByJavaScript(submitBtn); + waitUntilSpinnerDisappear(); + return this; + } + + @Step + public TopicDetails clickMessagesAddFiltersBtn() { + addFiltersBtn.shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicDetails clickNextButton() { + nextBtn.shouldBe(Condition.enabled).click(); + waitUntilSpinnerDisappear(); + return this; + } + + @Step + public TopicDetails openSavedFiltersListMdl() { + savedFiltersLink.shouldBe(Condition.enabled).click(); + backToCreateFiltersLink.shouldBe(Condition.visible); + return this; + } + + @Step + public boolean isFilterVisibleAtSavedFiltersMdl(String filterName) { + return isVisible($x(String.format(savedFilterNameLocator, filterName))); + } + + @Step + public TopicDetails selectFilterAtSavedFiltersMdl(String filterName) { + $x(String.format(savedFilterNameLocator, filterName)).shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicDetails clickSelectFilterBtnAtSavedFiltersMdl() { + selectFilterBtnAddFilterMdl.shouldBe(Condition.enabled).click(); + addFilterCodeModalTitle.shouldBe(Condition.disappear); + return this; + } + + @Step + public TopicDetails waitUntilAddFiltersMdlVisible() { + addFilterCodeModalTitle.shouldBe(Condition.visible); + return this; + } + + @Step + public TopicDetails setFilterCodeFieldAddFilterMdl(String filterCode) { + addFilterCodeInput.shouldBe(Condition.enabled).sendKeys(filterCode); + return this; + } + + @Step + public TopicDetails selectSaveThisFilterCheckboxMdl(boolean select) { + selectElement(saveThisFilterCheckBoxAddFilterMdl, select); + return this; + } + + @Step + public boolean isSaveThisFilterCheckBoxSelected() { + return isSelected(saveThisFilterCheckBoxAddFilterMdl); + } + + @Step + public TopicDetails setDisplayNameFldAddFilterMdl(String displayName) { + displayNameInputAddFilterMdl.shouldBe(Condition.enabled).sendKeys(displayName); + return this; + } + + @Step + public TopicDetails clickAddFilterBtnAndCloseMdl(boolean closeModal) { + addFilterBtnAddFilterMdl.shouldBe(Condition.enabled).click(); + if (closeModal) { + addFilterCodeModalTitle.shouldBe(Condition.hidden); + } else { + addFilterCodeModalTitle.shouldBe(Condition.visible); + } + return this; + } + + @Step + public boolean isAddFilterBtnAddFilterMdlEnabled() { + return isEnabled(addFilterBtnAddFilterMdl); + } + + @Step + public boolean isBackButtonEnabled() { + return isEnabled(backBtn); + } + + @Step + public boolean isNextButtonEnabled() { + return isEnabled(nextBtn); + } + + @Step + public boolean isActiveFilterVisible(String activeFilterName) { + return isVisible($x(String.format(activeFilterNameLocator, activeFilterName))); + } + + public List getAllAddFilterModalVisibleElements() { + return Arrays.asList(savedFiltersLink, displayNameInputAddFilterMdl, addFilterBtnAddFilterMdl, cancelBtnAddFilterMdl); + } + + public List getAllAddFilterModalEnabledElements() { + return Arrays.asList(displayNameInputAddFilterMdl, cancelBtnAddFilterMdl); + } + + public List getAllAddFilterModalDisabledElements() { + return Collections.singletonList(addFilterBtnAddFilterMdl); + } + + @Step + public TopicDetails openConsumerGroup(String consumerId) { + $x(String.format(consumerIdLocator, consumerId)).click(); + return this; + } + + @Step + public boolean isKeyMessageVisible(String keyMessage) { + return keyMessage.equals($("td[title]").getText()); + } + + @Step + public boolean isContentMessageVisible(String contentMessage) { + return contentMessage.matches(contentMessageTab.getText().trim()); + } + + private void selectYear(int expectedYear) { + while (getActualCalendarDate().getYear() > expectedYear) { + clickByJavaScript(previousMonthButton); + sleep(1000); + if (LocalTime.now().plusMinutes(3).isBefore(LocalTime.now())) { + throw new IllegalArgumentException("Unable to select year"); + } + } + } + + private void selectMonth(int expectedMonth) { + while (getActualCalendarDate().getMonthValue() > expectedMonth) { + clickByJavaScript(previousMonthButton); + sleep(1000); + if (LocalTime.now().plusMinutes(3).isBefore(LocalTime.now())) { + throw new IllegalArgumentException("Unable to select month"); + } + } + } + + private void selectDay(int expectedDay) { + Objects.requireNonNull($$x(String.format(dayCellLtr, expectedDay)).stream() + .filter(day -> !Objects.requireNonNull(day.getAttribute("class")).contains("outside-month")) + .findFirst().orElseThrow()).shouldBe(Condition.enabled).click(); + } + + private void setTime(LocalDateTime dateTime) { + calendarTimeFld.shouldBe(Condition.enabled) + .sendKeys(String.valueOf(dateTime.getHour()), String.valueOf(dateTime.getMinute())); + } + + @Step + public TopicDetails selectDateAndTimeByCalendar(LocalDateTime dateTime) { + setTime(dateTime); + selectYear(dateTime.getYear()); + selectMonth(dateTime.getMonthValue()); + selectDay(dateTime.getDayOfMonth()); + return this; + } + + private LocalDate getActualCalendarDate() { + String monthAndYearStr = actualCalendarDate.getText().trim(); + DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(DateTimeFormatter.ofPattern("MMMM yyyy")) + .toFormatter(Locale.ENGLISH); + YearMonth yearMonth = formatter.parse(monthAndYearStr, YearMonth::from); + return yearMonth.atDay(1); + } + + @Step + public TopicDetails openCalendarSeekType() { + seekTypeField.shouldBe(Condition.enabled).click(); + actualCalendarDate.shouldBe(Condition.visible); + return this; + } + + @Step + public int getMessageCountAmount() { + return Integer.parseInt(messageAmountCell.getText().trim()); + } + + private List initItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new TopicDetails.MessageGridItem(item))); + return gridItemList; + } + + @Step + public TopicDetails.MessageGridItem getMessageByOffset(int offset) { + return initItems().stream() + .filter(e -> e.getOffset() == offset) + .findFirst().orElseThrow(); + } + + @Step + public List getAllMessages() { + return initItems(); + } + + @Step + public TopicDetails.MessageGridItem getRandomMessage() { + return getMessageByOffset(nextInt(0, initItems().size() - 1)); + } + + public enum TopicMenu { + OVERVIEW("Overview"), + MESSAGES("Messages"), + CONSUMERS("Consumers"), + SETTINGS("Settings"); + + private final String value; + + TopicMenu(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } + + public static class MessageGridItem extends BasePage { + + private final SelenideElement element; + + private MessageGridItem(SelenideElement element) { + this.element = element; + } + + @Step + public MessageGridItem clickExpand() { + clickByJavaScript(element.$x("./td[1]/span")); + return this; + } + + private SelenideElement getOffsetElm() { + return element.$x("./td[2]"); + } + + @Step + public int getOffset() { + return Integer.parseInt(getOffsetElm().getText().trim()); + } + + @Step + public int getPartition() { + return Integer.parseInt(element.$x("./td[3]").getText().trim()); + } + + @Step + public LocalDateTime getTimestamp() { + String timestampValue = element.$x("./td[4]/div").getText().trim(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M/d/yyyy, HH:mm:ss"); + return LocalDateTime.parse(timestampValue, formatter); + } + + @Step + public String getKey() { + return element.$x("./td[5]").getText().trim(); + } + + @Step + public String getValue() { + return element.$x("./td[6]/span/p").getText().trim(); + } + + @Step + public MessageGridItem openDotMenu() { + getOffsetElm().hover(); + element.$x("./td[7]/div/button[@aria-label='Dropdown Toggle']") + .shouldBe(Condition.visible).click(); + return this; + } + + @Step + public MessageGridItem clickCopyToClipBoard() { + clickByJavaScript(element.$x("./td[7]//li[text() = 'Copy to clipboard']") + .shouldBe(Condition.visible)); + return this; + } + + @Step + public MessageGridItem clickSaveAsFile() { + clickByJavaScript(element.$x("./td[7]//li[text() = 'Save as a file']") + .shouldBe(Condition.visible)); + return this; + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicSettingsTab.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicSettingsTab.java new file mode 100644 index 0000000000..3c0fcac211 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicSettingsTab.java @@ -0,0 +1,66 @@ +package com.provectus.kafka.ui.pages.topics; + +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; + +import java.util.ArrayList; +import java.util.List; + +import static com.codeborne.selenide.Selenide.$x; + +public class TopicSettingsTab extends BasePage { + + protected SelenideElement defaultValueColumnHeaderLocator = $x("//div[text() = 'Default Value']"); + + @Step + public TopicSettingsTab waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + defaultValueColumnHeaderLocator.shouldBe(Condition.visible); + return this; + } + + private List initGridItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new SettingsGridItem(item))); + return gridItemList; + } + + private TopicSettingsTab.SettingsGridItem getItemByKey(String key) { + return initGridItems().stream() + .filter(e -> e.getKey().equals(key)) + .findFirst().orElseThrow(); + } + + @Step + public String getValueByKey(String key) { + return getItemByKey(key).getValue(); + } + + public static class SettingsGridItem extends BasePage { + + private final SelenideElement element; + + public SettingsGridItem(SelenideElement element) { + this.element = element; + } + + @Step + public String getKey() { + return element.$x("./td[1]/span").getText().trim(); + } + + @Step + public String getValue() { + return element.$x("./td[2]/span").getText().trim(); + } + + @Step + public String getDefaultValue() { + return element.$x("./td[3]/span").getText().trim(); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java new file mode 100644 index 0000000000..499d5fe965 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java @@ -0,0 +1,261 @@ +package com.provectus.kafka.ui.pages.topics; + +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.codeborne.selenide.Condition.visible; +import static com.codeborne.selenide.Selenide.$x; + +public class TopicsList extends BasePage { + + protected SelenideElement topicListHeader = $x("//h1[text()='Topics']"); + protected SelenideElement addTopicBtn = $x("//button[normalize-space(text()) ='Add a Topic']"); + protected SelenideElement searchField = $x("//input[@placeholder='Search by Topic Name']"); + protected SelenideElement showInternalRadioBtn = $x("//input[@name='ShowInternalTopics']"); + protected SelenideElement deleteSelectedTopicsBtn = $x("//button[text()='Delete selected topics']"); + protected SelenideElement copySelectedTopicBtn = $x("//button[text()='Copy selected topic']"); + protected SelenideElement purgeMessagesOfSelectedTopicsBtn = $x("//button[text()='Purge messages of selected topics']"); + protected SelenideElement clearMessagesBtn = $x("//ul[contains(@class ,'open')]//div[text()='Clear Messages']"); + protected SelenideElement recreateTopicBtn = $x("//ul[contains(@class ,'open')]//div[text()='Recreate Topic']"); + protected SelenideElement removeTopicBtn = $x("//ul[contains(@class ,'open')]//div[text()='Remove Topic']"); + + @Step + public TopicsList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + topicListHeader.shouldBe(visible); + return this; + } + + @Step + public TopicsList clickAddTopicBtn() { + clickByJavaScript(addTopicBtn); + return this; + } + + @Step + public boolean isTopicVisible(String topicName) { + tableGrid.shouldBe(visible); + return isVisible(getTableElement(topicName)); + } + + @Step + public boolean isShowInternalRadioBtnSelected() { + return isSelected(showInternalRadioBtn); + } + + @Step + public TopicsList setShowInternalRadioButton(boolean select) { + selectElement(showInternalRadioBtn, select); + return this; + } + + @Step + public TopicsList openTopic(String topicName) { + getTopicItem(topicName).openItem(); + return this; + } + + @Step + public TopicsList openDotMenuByTopicName(String topicName) { + getTopicItem(topicName).openDotMenu(); + return this; + } + + @Step + public boolean isCopySelectedTopicBtnEnabled() { + return isEnabled(copySelectedTopicBtn); + } + + @Step + public List getActionButtons() { + return Stream.of(deleteSelectedTopicsBtn, copySelectedTopicBtn, purgeMessagesOfSelectedTopicsBtn) + .collect(Collectors.toList()); + } + + @Step + public TopicsList clickCopySelectedTopicBtn() { + copySelectedTopicBtn.shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicsList clickPurgeMessagesOfSelectedTopicsBtn() { + purgeMessagesOfSelectedTopicsBtn.shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicsList clickClearMessagesBtn() { + clickByJavaScript(clearMessagesBtn.shouldBe(visible)); + return this; + } + + @Step + public TopicsList clickRecreateTopicBtn() { + clickByJavaScript(recreateTopicBtn.shouldBe(visible)); + return this; + } + + @Step + public TopicsList clickRemoveTopicBtn() { + clickByJavaScript(removeTopicBtn.shouldBe(visible)); + return this; + } + + @Step + public TopicsList clickConfirmBtnMdl() { + clickConfirmButton(); + return this; + } + + @Step + public TopicsList clickCancelBtnMdl() { + clickCancelButton(); + return this; + } + + @Step + public boolean isConfirmationMdlVisible() { + return isConfirmationModalVisible(); + } + + @Step + public boolean isAlertWithMessageVisible(AlertHeader header, String message) { + return isAlertVisible(header, message); + } + + private List getVisibleColumnHeaders() { + return Stream.of("Replication Factor", "Number of messages", "Topic Name", "Partitions", "Out of sync replicas", "Size") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } + + private List getEnabledColumnHeaders() { + return Stream.of("Topic Name", "Partitions", "Out of sync replicas", "Size") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } + + @Step + public List getAllVisibleElements() { + List visibleElements = new ArrayList<>(getVisibleColumnHeaders()); + visibleElements.addAll(Arrays.asList(searchField, addTopicBtn, tableGrid)); + visibleElements.addAll(getActionButtons()); + return visibleElements; + } + + @Step + public List getAllEnabledElements() { + List enabledElements = new ArrayList<>(getEnabledColumnHeaders()); + enabledElements.addAll(Arrays.asList(searchField, showInternalRadioBtn, addTopicBtn)); + return enabledElements; + } + + private List initGridItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new TopicGridItem(item))); + return gridItemList; + } + + @Step + public TopicGridItem getTopicItem(String name) { + return initGridItems().stream() + .filter(e -> e.getName().equals(name)) + .findFirst().orElseThrow(); + } + + @Step + public List getNonInternalTopics() { + return initGridItems().stream() + .filter(e -> !e.isInternal()) + .collect(Collectors.toList()); + } + + @Step + public List getInternalTopics() { + return initGridItems().stream() + .filter(TopicGridItem::isInternal) + .collect(Collectors.toList()); + } + + public static class TopicGridItem extends BasePage { + + private final SelenideElement element; + + public TopicGridItem(SelenideElement element) { + this.element = element; + } + + @Step + public TopicsList selectItem(boolean select) { + selectElement(element.$x("./td[1]/input"), select); + return new TopicsList(); + } + + @Step + public boolean isInternal() { + boolean internal = false; + try { + element.$x("./td[2]/a/span").shouldBe(visible, Duration.ofMillis(500)); + internal = true; + } catch (Throwable ignored) { + } + return internal; + } + + private SelenideElement getNameElm() { + return element.$x("./td[2]"); + } + + @Step + public String getName() { + return getNameElm().getText().trim(); + } + + @Step + public void openItem() { + getNameElm().click(); + } + + @Step + public int getPartition() { + return Integer.parseInt(element.$x("./td[3]").getText().trim()); + } + + @Step + public int getOutOfSyncReplicas() { + return Integer.parseInt(element.$x("./td[4]").getText().trim()); + } + + @Step + public int getReplicationFactor() { + return Integer.parseInt(element.$x("./td[5]").getText().trim()); + } + + @Step + public int getNumberOfMessages() { + return Integer.parseInt(element.$x("./td[6]").getText().trim()); + } + + @Step + public int getSize() { + return Integer.parseInt(element.$x("./td[7]").getText().trim()); + } + + @Step + public void openDotMenu() { + element.$x("./td[8]//button").click(); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CleanupPolicyValue.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CleanupPolicyValue.java new file mode 100644 index 0000000000..48c0c0fbcb --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CleanupPolicyValue.java @@ -0,0 +1,25 @@ +package com.provectus.kafka.ui.pages.topics.enums; + +public enum CleanupPolicyValue { + + DELETE("delete", "Delete"), + COMPACT("compact", "Compact"), + COMPACT_DELETE("compact,delete", "Compact,Delete"); + + private final String optionValue; + private final String visibleText; + + CleanupPolicyValue(String optionValue, String visibleText) { + this.optionValue = optionValue; + this.visibleText = visibleText; + } + + public String getOptionValue() { + return optionValue; + } + + public String getVisibleText() { + return visibleText; + } +} + diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CustomParameterType.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CustomParameterType.java new file mode 100644 index 0000000000..f4cb5cb951 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CustomParameterType.java @@ -0,0 +1,37 @@ +package com.provectus.kafka.ui.pages.topics.enums; + +public enum CustomParameterType { + + COMPRESSION_TYPE("compression.type"), + DELETE_RETENTION_MS("delete.retention.ms"), + FILE_DELETE_DELAY_MS("file.delete.delay.ms"), + FLUSH_MESSAGES("flush.messages"), + FLUSH_MS("flush.ms"), + FOLLOWER_REPLICATION_THROTTLED_REPLICAS("follower.replication.throttled.replicas"), + INDEX_INTERVAL_BYTES("index.interval.bytes"), + LEADER_REPLICATION_THROTTLED_REPLICAS("leader.replication.throttled.replicas"), + MAX_COMPACTION_LAG_MS("max.compaction.lag.ms"), + MESSAGE_DOWNCONVERSION_ENABLE("message.downconversion.enable"), + MESSAGE_FORMAT_VERSION("message.format.version"), + MESSAGE_TIMESTAMP_DIFFERENCE_MAX_MS("message.timestamp.difference.max.ms"), + MESSAGE_TIMESTAMP_TYPE("message.timestamp.type"), + MIN_CLEANABLE_DIRTY_RATIO("min.cleanable.dirty.ratio"), + MIN_COMPACTION_LAG_MS("min.compaction.lag.ms"), + PREALLOCATE("preallocate"), + RETENTION_BYTES("retention.bytes"), + SEGMENT_BYTES("segment.bytes"), + SEGMENT_INDEX_BYTES("segment.index.bytes"), + SEGMENT_JITTER_MS("segment.jitter.ms"), + SEGMENT_MS("segment.ms"), + UNCLEAN_LEADER_ELECTION_ENABLE("unclean.leader.election.enable"); + + private final String optionValue; + + CustomParameterType(String optionValue) { + this.optionValue = optionValue; + } + + public String getOptionValue() { + return optionValue; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/MaxSizeOnDisk.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/MaxSizeOnDisk.java new file mode 100644 index 0000000000..8f459eea75 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/MaxSizeOnDisk.java @@ -0,0 +1,27 @@ +package com.provectus.kafka.ui.pages.topics.enums; + +public enum MaxSizeOnDisk { + + NOT_SET("-1", "Not Set"), + SIZE_1_GB("1073741824", "1 GB"), + SIZE_10_GB("10737418240", "10 GB"), + SIZE_20_GB("21474836480", "20 GB"), + SIZE_50_GB("53687091200", "50 GB"); + + private final String optionValue; + private final String visibleText; + + MaxSizeOnDisk(String optionValue, String visibleText) { + this.optionValue = optionValue; + this.visibleText = visibleText; + } + + public String getOptionValue() { + return optionValue; + } + + public String getVisibleText() { + return visibleText; + } +} + diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/TimeToRetain.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/TimeToRetain.java new file mode 100644 index 0000000000..c07abdc175 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/TimeToRetain.java @@ -0,0 +1,26 @@ +package com.provectus.kafka.ui.pages.topics.enums; + +public enum TimeToRetain { + + BTN_12_HOURS("12 hours", "43200000"), + BTN_1_DAY("1 day", "86400000"), + BTN_2_DAYS("2 days", "172800000"), + BTN_7_DAYS("7 days", "604800000"), + BTN_4_WEEKS("4 weeks", "2419200000"); + + private final String button; + private final String value; + + TimeToRetain(String button, String value) { + this.button = button; + this.value = value; + } + + public String getButton() { + return button; + } + + public String getValue() { + return value; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java index 8451ef836e..54f69b5198 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java @@ -1,71 +1,75 @@ package com.provectus.kafka.ui.services; -import static com.codeborne.selenide.Selenide.sleep; -import static com.provectus.kafka.ui.utilities.FileUtils.fileToString; - import com.fasterxml.jackson.databind.ObjectMapper; import com.provectus.kafka.ui.api.ApiClient; -import com.provectus.kafka.ui.api.api.KafkaConnectApi; -import com.provectus.kafka.ui.api.api.MessagesApi; -import com.provectus.kafka.ui.api.api.SchemasApi; -import com.provectus.kafka.ui.api.api.TopicsApi; -import com.provectus.kafka.ui.api.model.CreateTopicMessage; -import com.provectus.kafka.ui.api.model.NewConnector; -import com.provectus.kafka.ui.api.model.NewSchemaSubject; -import com.provectus.kafka.ui.api.model.TopicCreation; +import com.provectus.kafka.ui.api.api.*; +import com.provectus.kafka.ui.api.model.*; import com.provectus.kafka.ui.models.Connector; import com.provectus.kafka.ui.models.Schema; import com.provectus.kafka.ui.models.Topic; +import com.provectus.kafka.ui.pages.ksqlDb.models.Stream; +import com.provectus.kafka.ui.pages.ksqlDb.models.Table; import com.provectus.kafka.ui.settings.BaseSource; import io.qameta.allure.Step; -import java.util.HashMap; -import java.util.Map; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.web.reactive.function.client.WebClientResponseException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static com.codeborne.selenide.Selenide.sleep; +import static com.provectus.kafka.ui.utilities.FileUtils.fileToString; + @Slf4j public class ApiService extends BaseSource { @SneakyThrows private TopicsApi topicApi() { - return new TopicsApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); + return new TopicsApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); } @SneakyThrows private SchemasApi schemaApi() { - return new SchemasApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); + return new SchemasApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); } @SneakyThrows private KafkaConnectApi connectorApi() { - return new KafkaConnectApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); + return new KafkaConnectApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); } @SneakyThrows private MessagesApi messageApi() { - return new MessagesApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); + return new MessagesApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); + } + + @SneakyThrows + private KsqlApi ksqlApi() { + return new KsqlApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); } @SneakyThrows private void createTopic(String clusterName, String topicName) { - TopicCreation topic = new TopicCreation(); - topic.setName(topicName); - topic.setPartitions(1); - topic.setReplicationFactor(1); - try { - topicApi().createTopic(clusterName, topic).block(); - sleep(2000); - } catch (WebClientResponseException ex) { - ex.printStackTrace(); - } + TopicCreation topic = new TopicCreation(); + topic.setName(topicName); + topic.setPartitions(1); + topic.setReplicationFactor(1); + try { + topicApi().createTopic(clusterName, topic).block(); + sleep(2000); + } catch (WebClientResponseException ex) { + ex.printStackTrace(); + } } @Step public ApiService createTopic(String topicName) { - createTopic(CLUSTER_NAME, topicName); - return this; + createTopic(CLUSTER_NAME, topicName); + return this; } @SneakyThrows @@ -77,9 +81,9 @@ public class ApiService extends BaseSource { } @Step - public ApiService deleteTopic(String topicName){ - deleteTopic(CLUSTER_NAME, topicName); - return this; + public ApiService deleteTopic(String topicName) { + deleteTopic(CLUSTER_NAME, topicName); + return this; } @SneakyThrows @@ -96,9 +100,9 @@ public class ApiService extends BaseSource { } @Step - public ApiService createSchema(Schema schema){ - createSchema(CLUSTER_NAME, schema); - return this; + public ApiService createSchema(Schema schema) { + createSchema(CLUSTER_NAME, schema); + return this; } @SneakyThrows @@ -110,9 +114,9 @@ public class ApiService extends BaseSource { } @Step - public ApiService deleteSchema(String schemaName){ - deleteSchema(CLUSTER_NAME, schemaName); - return this; + public ApiService deleteSchema(String schemaName) { + deleteSchema(CLUSTER_NAME, schemaName); + return this; } @SneakyThrows @@ -124,9 +128,9 @@ public class ApiService extends BaseSource { } @Step - public ApiService deleteConnector(String connectName, String connectorName){ - deleteConnector(CLUSTER_NAME, connectName, connectorName); - return this; + public ApiService deleteConnector(String connectName, String connectorName) { + deleteConnector(CLUSTER_NAME, connectName, connectorName); + return this; } @SneakyThrows @@ -143,9 +147,9 @@ public class ApiService extends BaseSource { } @Step - public ApiService createConnector(String connectName, Connector connector){ - createConnector(CLUSTER_NAME, connectName, connector); - return this; + public ApiService createConnector(String connectName, Connector connector) { + createConnector(CLUSTER_NAME, connectName, connector); + return this; } @Step @@ -155,22 +159,102 @@ public class ApiService extends BaseSource { @SneakyThrows private void sendMessage(String clusterName, Topic topic) { - CreateTopicMessage createMessage = new CreateTopicMessage(); - createMessage.setPartition(0); - createMessage.setKeySerde("String"); - createMessage.setValueSerde("String"); - createMessage.setKey(topic.getMessageKey()); - createMessage.setContent(topic.getMessageContent()); - try { - messageApi().sendTopicMessages(clusterName, topic.getName(), createMessage).block(); - } catch (WebClientResponseException ex) { - ex.getRawStatusCode(); - } + CreateTopicMessage createMessage = new CreateTopicMessage(); + createMessage.setPartition(0); + createMessage.setKeySerde("String"); + createMessage.setValueSerde("String"); + createMessage.setKey(topic.getMessageKey()); + createMessage.setContent(topic.getMessageContent()); + try { + messageApi().sendTopicMessages(clusterName, topic.getName(), createMessage).block(); + } catch (WebClientResponseException ex) { + ex.getRawStatusCode(); + } } @Step public ApiService sendMessage(Topic topic) { - sendMessage(CLUSTER_NAME, topic); - return this; + sendMessage(CLUSTER_NAME, topic); + return this; + } + + @Step + public ApiService createStream(Stream stream) { + KsqlCommandV2Response pipeIdStream = ksqlApi() + .executeKsql(CLUSTER_NAME, new KsqlCommandV2() + .ksql(String.format("CREATE STREAM %s (profileId VARCHAR, latitude DOUBLE, longitude DOUBLE) ", + stream.getName()) + + String.format("WITH (kafka_topic='%s', value_format='json', partitions=1);", + stream.getTopicName()))) + .block(); + assert pipeIdStream != null; + List responseListStream = ksqlApi() + .openKsqlResponsePipe(CLUSTER_NAME, pipeIdStream.getPipeId()) + .collectList() + .block(); + assert Objects.requireNonNull(responseListStream).size() != 0; + return this; + } + + @Step + public ApiService createTables(Table firstTable, Table secondTable) { + KsqlCommandV2Response pipeIdTable1 = ksqlApi() + .executeKsql(CLUSTER_NAME, new KsqlCommandV2() + .ksql(String.format("CREATE TABLE %s AS ", firstTable.getName()) + + " SELECT profileId, " + + " LATEST_BY_OFFSET(latitude) AS la, " + + " LATEST_BY_OFFSET(longitude) AS lo " + + String.format(" FROM %s ", firstTable.getStreamName()) + + " GROUP BY profileId " + + " EMIT CHANGES;")) + .block(); + assert pipeIdTable1 != null; + List responseListTable = ksqlApi() + .openKsqlResponsePipe(CLUSTER_NAME, pipeIdTable1.getPipeId()) + .collectList() + .block(); + assert Objects.requireNonNull(responseListTable).size() != 0; + KsqlCommandV2Response pipeIdTable2 = ksqlApi() + .executeKsql(CLUSTER_NAME, new KsqlCommandV2() + .ksql(String.format("CREATE TABLE %s AS ", secondTable.getName()) + + " SELECT ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1) AS distanceInMiles, " + + " COLLECT_LIST(profileId) AS riders, " + + " COUNT(*) AS count " + + String.format(" FROM %s ", firstTable.getName()) + + " GROUP BY ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1);")) + .block(); + assert pipeIdTable2 != null; + List responseListTable2 = ksqlApi() + .openKsqlResponsePipe(CLUSTER_NAME, pipeIdTable2.getPipeId()) + .collectList() + .block(); + assert Objects.requireNonNull(responseListTable2).size() != 0; + return this; + } + + @Step + public ApiService insertInto(Stream stream) { + String streamName = stream.getName(); + KsqlCommandV2Response pipeIdInsert = ksqlApi() + .executeKsql(CLUSTER_NAME, new KsqlCommandV2() + .ksql("INSERT INTO " + streamName + " (profileId, latitude, longitude) VALUES ('c2309eec', 37.7877, -122.4205);" + + "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('18f4ea86', 37.3903, -122.0643); " + + "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('4ab5cbad', 37.3952, -122.0813); " + + "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('8b6eae59', 37.3944, -122.0813); " + + "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('4a7c7b41', 37.4049, -122.0822); " + + "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('4ddad000', 37.7857, -122.4011);")) + .block(); + assert pipeIdInsert != null; + List responseListInsert = ksqlApi() + .openKsqlResponsePipe(CLUSTER_NAME, pipeIdInsert.getPipeId()) + .collectList() + .block(); + assert Objects.requireNonNull(responseListInsert).size() != 0; + return this; } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java index e108d4e554..ebead7b089 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java @@ -5,16 +5,17 @@ import org.aeonbits.owner.ConfigFactory; public abstract class BaseSource { - private static Config config; - public static final String BASE_CONTAINER_URL = "http://host.testcontainers.internal:8080"; - public static final String BASE_LOCAL_URL = "http://localhost:8080"; - public static final String CLUSTER_NAME = "local"; - public static final String BROWSER = config().browser(); + public static final String BASE_CONTAINER_URL = "http://host.testcontainers.internal:8080"; + public static final String BASE_LOCAL_URL = "http://localhost:8080"; + public static final String CLUSTER_NAME = "local"; + private static Config config; + public static final String BROWSER = config().browser(); + public static final String SUITE_NAME = config().suite(); - private static Config config() { - if (config == null) { - config = ConfigFactory.create(Config.class, System.getProperties()); + private static Config config() { + if (config == null) { + config = ConfigFactory.create(Config.class, System.getProperties()); + } + return config; } - return config; - } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Profiles.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Profiles.java index 9a5ca69107..ef61d7d770 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Profiles.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Profiles.java @@ -2,12 +2,16 @@ package com.provectus.kafka.ui.settings.configs; import org.aeonbits.owner.Config; +import static com.provectus.kafka.ui.variables.Browser.CONTAINER; +import static com.provectus.kafka.ui.variables.Suite.CUSTOM; + public interface Profiles extends Config { - String CONTAINER = "container"; - String LOCAL = "local"; + @Key("browser") + @DefaultValue(CONTAINER) + String browser(); - @Key("browser") - @DefaultValue(CONTAINER) - String browser(); + @Key("suite") + @DefaultValue(CUSTOM) + String suite(); } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/AllureListener.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/AllureListener.java new file mode 100644 index 0000000000..74119f8480 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/AllureListener.java @@ -0,0 +1,35 @@ +package com.provectus.kafka.ui.settings.listeners; + +import com.codeborne.selenide.Screenshots; +import io.qameta.allure.Allure; +import io.qameta.allure.testng.AllureTestNg; +import org.testng.ITestListener; +import org.testng.ITestResult; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; + +import static java.nio.file.Files.newInputStream; + +public class AllureListener extends AllureTestNg implements ITestListener { + + private void takeScreenshot() { + File screenshot = Screenshots.takeScreenShotAsFile(); + try { + Allure.addAttachment(Objects.requireNonNull(screenshot).getName(), newInputStream(screenshot.toPath())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void onTestFailure(ITestResult result) { + takeScreenshot(); + } + + @Override + public void onTestSkipped(ITestResult result) { + takeScreenshot(); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/LoggerListener.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/LoggerListener.java new file mode 100644 index 0000000000..ca096cd238 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/LoggerListener.java @@ -0,0 +1,37 @@ +package com.provectus.kafka.ui.settings.listeners; + +import lombok.extern.slf4j.Slf4j; +import org.testng.ITestResult; +import org.testng.TestListenerAdapter; + +@Slf4j +public class LoggerListener extends TestListenerAdapter { + + @Override + public void onTestStart(final ITestResult testResult) { + log.info(String.format("\n------------------------------------------------------------------------ " + + "\nTEST STARTED: %s.%s \n------------------------------------------------------------------------ \n", + testResult.getInstanceName(), testResult.getName())); + } + + @Override + public void onTestSuccess(final ITestResult testResult) { + log.info(String.format("\n------------------------------------------------------------------------ " + + "\nTEST PASSED: %s.%s \n------------------------------------------------------------------------ \n", + testResult.getInstanceName(), testResult.getName())); + } + + @Override + public void onTestFailure(final ITestResult testResult) { + log.info(String.format("\n------------------------------------------------------------------------ " + + "\nTEST FAILED: %s.%s \n------------------------------------------------------------------------ \n", + testResult.getInstanceName(), testResult.getName())); + } + + @Override + public void onTestSkipped(final ITestResult testResult) { + log.info(String.format("\n------------------------------------------------------------------------ " + + "\nTEST SKIPPED: %s.%s \n------------------------------------------------------------------------ \n", + testResult.getInstanceName(), testResult.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseCreateListener.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseCreateListener.java new file mode 100644 index 0000000000..c40481c300 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseCreateListener.java @@ -0,0 +1,123 @@ +package com.provectus.kafka.ui.settings.listeners; + +import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; +import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Status; +import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Suite; +import io.qase.api.QaseClient; +import io.qase.api.StepStorage; +import io.qase.api.annotation.QaseId; +import io.qase.client.ApiClient; +import io.qase.client.api.CasesApi; +import io.qase.client.model.*; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.testng.Assert; +import org.testng.ITestListener; +import org.testng.ITestResult; +import org.testng.TestListenerAdapter; + +import java.lang.reflect.Method; +import java.util.*; + +import static io.qase.api.utils.IntegrationUtils.getCaseTitle; + +@Slf4j +public class QaseCreateListener extends TestListenerAdapter implements ITestListener { + + private static final CasesApi QASE_API = getQaseApi(); + + private static CasesApi getQaseApi() { + ApiClient apiClient = QaseClient.getApiClient(); + apiClient.setApiKey(System.getProperty("QASEIO_API_TOKEN")); + return new CasesApi(apiClient); + } + + private static int getStatus(Method method) { + if (method.isAnnotationPresent(Status.class)) + return method.getDeclaredAnnotation(Status.class).status().getValue(); + return 1; + } + + private static int getAutomation(Method method) { + if (method.isAnnotationPresent(Automation.class)) + return method.getDeclaredAnnotation(Automation.class).state().getValue(); + return 0; + } + + @SneakyThrows + private static HashMap getCaseTitlesAndIdsFromQase() { + HashMap cases = new HashMap<>(); + boolean getCases = true; + int offSet = 0; + while (getCases) { + getCases = false; + TestCaseListResponse response = QASE_API.getCases(System.getProperty("QASE_PROJECT_CODE"), + new GetCasesFiltersParameter().status(GetCasesFiltersParameter.SERIALIZED_NAME_STATUS), 100, offSet); + TestCaseListResponseAllOfResult result = response.getResult(); + Assert.assertNotNull(result); + List entities = result.getEntities(); + Assert.assertNotNull(entities); + if (entities.size() > 0) { + for (TestCase testCase : entities) { + cases.put(testCase.getId(), testCase.getTitle()); + } + offSet = offSet + 100; + getCases = true; + } + } + return cases; + } + + private static boolean isCaseWithTitleExistInQase(Method method) { + HashMap cases = getCaseTitlesAndIdsFromQase(); + String title = getCaseTitle(method); + if (cases.containsValue(title)) { + for (Map.Entry map : cases.entrySet()) { + if (map.getValue().matches(title)) { + long id = map.getKey(); + log.warn(String.format("Test case with @QaseTitle='%s' already exists with @QaseId=%d. " + + "Please verify @QaseTitle annotation", title, id)); + return true; + } + } + } + return false; + } + + @Override + @SneakyThrows + public void onTestSuccess(final ITestResult testResult) { + Method method = testResult.getMethod() + .getConstructorOrMethod() + .getMethod(); + String title = getCaseTitle(method); + if (!method.isAnnotationPresent(QaseId.class)) { + if (title != null) { + if (!isCaseWithTitleExistInQase(method)) { + LinkedList resultSteps = StepStorage.stopSteps(); + LinkedList createSteps = new LinkedList<>(); + resultSteps.forEach(step -> { + TestCaseCreateStepsInner caseStep = new TestCaseCreateStepsInner(); + caseStep.setAction(step.getAction()); + caseStep.setExpectedResult(step.getExpectedResult()); + createSteps.add(caseStep); + }); + TestCaseCreate newCase = new TestCaseCreate(); + newCase.setTitle(title); + newCase.setStatus(getStatus(method)); + newCase.setAutomation(getAutomation(method)); + newCase.setSteps(createSteps); + if (method.isAnnotationPresent(Suite.class)) { + long suiteId = method.getDeclaredAnnotation(Suite.class).id(); + newCase.suiteId(suiteId); + } + Long id = Objects.requireNonNull(QASE_API.createCase(System.getProperty("QASE_PROJECT_CODE"), + newCase).getResult()).getId(); + log.info(String.format("New test case '%s' was created with @QaseId=%d", title, id)); + } + } else + log.warn("To create new test case in Qase.io please add @QaseTitle annotation"); + } else + log.warn("To create new test case in Qase.io please remove @QaseId annotation"); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseResultListener.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseResultListener.java new file mode 100644 index 0000000000..abab897bfc --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseResultListener.java @@ -0,0 +1,102 @@ +package com.provectus.kafka.ui.settings.listeners; + +import io.qase.api.StepStorage; +import io.qase.api.config.QaseConfig; +import io.qase.api.services.QaseTestCaseListener; +import io.qase.client.model.ResultCreate; +import io.qase.client.model.ResultCreateCase; +import io.qase.client.model.ResultCreateStepsInner; +import io.qase.testng.guice.module.TestNgModule; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.testng.ITestContext; +import org.testng.ITestListener; +import org.testng.ITestResult; +import org.testng.TestListenerAdapter; + +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.Optional; + +import static io.qase.api.utils.IntegrationUtils.*; +import static io.qase.client.model.ResultCreate.StatusEnum.*; + +@Slf4j +public class QaseResultListener extends TestListenerAdapter implements ITestListener { + + private static final String REPORTER_NAME = "TestNG"; + + static { + System.setProperty(QaseConfig.QASE_CLIENT_REPORTER_NAME_KEY, REPORTER_NAME); + } + + @Getter(lazy = true, value = AccessLevel.PRIVATE) + private final QaseTestCaseListener qaseTestCaseListener = createQaseListener(); + + private static QaseTestCaseListener createQaseListener() { + return TestNgModule.getInjector().getInstance(QaseTestCaseListener.class); + } + + @Override + public void onTestStart(ITestResult result) { + getQaseTestCaseListener().onTestCaseStarted(); + super.onTestStart(result); + } + + @Override + public void onTestSuccess(ITestResult tr) { + getQaseTestCaseListener() + .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, PASSED)); + super.onTestSuccess(tr); + } + + @Override + public void onTestSkipped(ITestResult tr) { + getQaseTestCaseListener() + .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, SKIPPED)); + super.onTestSuccess(tr); + } + + @Override + public void onTestFailure(ITestResult tr) { + getQaseTestCaseListener() + .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, FAILED)); + super.onTestFailure(tr); + } + + @Override + public void onFinish(ITestContext testContext) { + getQaseTestCaseListener().onTestCasesSetFinished(); + super.onFinish(testContext); + } + + private void setupResultItem(ResultCreate resultCreate, ITestResult result, ResultCreate.StatusEnum status) { + Optional resultThrowable = Optional.ofNullable(result.getThrowable()); + String comment = resultThrowable + .flatMap(throwable -> Optional.of(throwable.toString())).orElse(null); + Boolean isDefect = resultThrowable + .flatMap(throwable -> Optional.of(throwable instanceof AssertionError)) + .orElse(false); + String stacktrace = resultThrowable + .flatMap(throwable -> Optional.of(getStacktrace(throwable))) + .orElse(null); + Method method = result.getMethod() + .getConstructorOrMethod() + .getMethod(); + Long caseId = getCaseId(method); + String caseTitle = null; + if (caseId == null) { + caseTitle = getCaseTitle(method); + } + LinkedList steps = StepStorage.stopSteps(); + resultCreate + ._case(caseTitle == null ? null : new ResultCreateCase().title(caseTitle)) + .caseId(caseId) + .status(status) + .comment(comment) + .stacktrace(stacktrace) + .steps(steps.isEmpty() ? null : steps) + .defect(isDefect); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/TimeUtils.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/TimeUtils.java index 259f70c3cc..f53e8897e9 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/TimeUtils.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/TimeUtils.java @@ -1,16 +1,17 @@ package com.provectus.kafka.ui.utilities; -import static com.codeborne.selenide.Selenide.sleep; +import lombok.extern.slf4j.Slf4j; import java.time.LocalTime; -import lombok.extern.slf4j.Slf4j; + +import static com.codeborne.selenide.Selenide.sleep; @Slf4j public class TimeUtils { - public static void waitUntilNewMinuteStarted(){ - int secondsLeft = 60 - LocalTime.now().getSecond(); - log.debug("\nwaitUntilNewMinuteStarted: {}s", secondsLeft); - sleep(secondsLeft * 1000); - } + public static void waitUntilNewMinuteStarted() { + int secondsLeft = 60 - LocalTime.now().getSecond(); + log.debug("\nwaitUntilNewMinuteStarted: {}s", secondsLeft); + sleep(secondsLeft * 1000); + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java index 307361ad5d..7e0de1ac65 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java @@ -1,7 +1,5 @@ package com.provectus.kafka.ui.utilities; -import static com.codeborne.selenide.Selenide.executeJavaScript; - import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.WebDriverRunner; @@ -9,73 +7,75 @@ import lombok.extern.slf4j.Slf4j; import org.openqa.selenium.Keys; import org.openqa.selenium.interactions.Actions; +import static com.codeborne.selenide.Selenide.executeJavaScript; + @Slf4j public class WebUtils { - public static void clickByActions(SelenideElement element) { - log.debug("\nclickByActions: {}", element.getSearchCriteria()); - element.shouldBe(Condition.enabled); - new Actions(WebDriverRunner.getWebDriver()) - .moveToElement(element) - .click(element) - .perform(); - } - - public static void clickByJavaScript(SelenideElement element) { - log.debug("\nclickByJavaScript: {}", element.getSearchCriteria()); - element.shouldBe(Condition.enabled); - String script = "arguments[0].click();"; - executeJavaScript(script, element); - } - - public static void clearByKeyboard(SelenideElement field) { - log.debug("\nclearByKeyboard: {}", field.getSearchCriteria()); - field.shouldBe(Condition.enabled).sendKeys(Keys.END); - field.sendKeys(Keys.chord(Keys.CONTROL + "a"), Keys.DELETE); - } - - public static boolean isVisible(SelenideElement element) { - log.debug("\nisVisible: {}", element.getSearchCriteria()); - boolean isVisible = false; - try { - element.shouldBe(Condition.visible); - isVisible = true; - } catch (Throwable e) { - log.debug("{} is not visible", element.getSearchCriteria()); + public static void clickByActions(SelenideElement element) { + log.debug("\nclickByActions: {}", element.getSearchCriteria()); + element.shouldBe(Condition.enabled); + new Actions(WebDriverRunner.getWebDriver()) + .moveToElement(element) + .click(element) + .perform(); } - return isVisible; - } - public static boolean isEnabled(SelenideElement element) { - log.debug("\nisEnabled: {}", element.getSearchCriteria()); - boolean isEnabled = false; - try { - element.shouldBe(Condition.enabled); - isEnabled = true; - } catch (Throwable e) { - log.debug("{} is not enabled", element.getSearchCriteria()); + public static void clickByJavaScript(SelenideElement element) { + log.debug("\nclickByJavaScript: {}", element.getSearchCriteria()); + element.shouldBe(Condition.enabled); + String script = "arguments[0].click();"; + executeJavaScript(script, element); } - return isEnabled; - } - public static boolean isSelected(SelenideElement element) { - log.debug("\nisSelected: {}", element.getSearchCriteria()); - boolean isSelected = false; - try { - element.shouldBe(Condition.selected); - isSelected = true; - } catch (Throwable e) { - log.debug("{} is not selected", element.getSearchCriteria()); + public static void clearByKeyboard(SelenideElement field) { + log.debug("\nclearByKeyboard: {}", field.getSearchCriteria()); + field.shouldBe(Condition.enabled).sendKeys(Keys.END); + field.sendKeys(Keys.chord(Keys.CONTROL + "a"), Keys.DELETE); } - return isSelected; - } - public static boolean selectElement(SelenideElement element, boolean select){ - if (select) { - if (!element.isSelected()) clickByJavaScript(element); - } else { - if (element.isSelected()) clickByJavaScript(element); + public static boolean isVisible(SelenideElement element) { + log.debug("\nisVisible: {}", element.getSearchCriteria()); + boolean isVisible = false; + try { + element.shouldBe(Condition.visible); + isVisible = true; + } catch (Throwable e) { + log.debug("{} is not visible", element.getSearchCriteria()); + } + return isVisible; + } + + public static boolean isEnabled(SelenideElement element) { + log.debug("\nisEnabled: {}", element.getSearchCriteria()); + boolean isEnabled = false; + try { + element.shouldBe(Condition.enabled); + isEnabled = true; + } catch (Throwable e) { + log.debug("{} is not enabled", element.getSearchCriteria()); + } + return isEnabled; + } + + public static boolean isSelected(SelenideElement element) { + log.debug("\nisSelected: {}", element.getSearchCriteria()); + boolean isSelected = false; + try { + element.shouldBe(Condition.selected); + isSelected = true; + } catch (Throwable e) { + log.debug("{} is not selected", element.getSearchCriteria()); + } + return isSelected; + } + + public static boolean selectElement(SelenideElement element, boolean select) { + if (select) { + if (!element.isSelected()) clickByJavaScript(element); + } else { + if (element.isSelected()) clickByJavaScript(element); + } + return true; } - return true; - } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/DisplayNameGenerator.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/DisplayNameGenerator.java deleted file mode 100644 index 15d239983c..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/DisplayNameGenerator.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.provectus.kafka.ui.utilities.qaseIoUtils; - -import org.junit.platform.commons.util.ClassUtils; -import org.junit.platform.commons.util.Preconditions; - -import java.lang.reflect.Method; - -public class DisplayNameGenerator implements org.junit.jupiter.api.DisplayNameGenerator { - @Override - public String generateDisplayNameForClass(Class testClass) { - String name = testClass.getName(); - int lastDot = name.lastIndexOf('.'); - return name.substring(lastDot + 1).replaceAll("([A-Z])", " $1").toLowerCase(); - } - - @Override - public String generateDisplayNameForNestedClass(Class nestedClass) { - return nestedClass.getSimpleName(); - } - - @Override - public String generateDisplayNameForMethod(Class testClass, Method testMethod) { - return testMethod.getName().replaceAll("([A-Z])", " $1").toLowerCase() - + parameterTypesAsString(testMethod); - } - - static String parameterTypesAsString(Method method) { - Preconditions.notNull(method, "Method must not be null"); - return method.getParameterTypes().length == 0 - ? "" - : '(' + ClassUtils.nullSafeToString(Class::getSimpleName, method.getParameterTypes()) + ')'; - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/QaseExtension.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/QaseExtension.java deleted file mode 100644 index 474acd1099..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/QaseExtension.java +++ /dev/null @@ -1,144 +0,0 @@ -package com.provectus.kafka.ui.utilities.qaseIoUtils; - -import io.qase.api.QaseClient; -import io.qase.api.StepStorage; -import io.qase.api.exceptions.QaseException; -import io.qase.client.ApiClient; -import io.qase.client.api.ResultsApi; -import io.qase.client.model.ResultCreate; -import io.qase.client.model.ResultCreate.StatusEnum; -import io.qase.client.model.ResultCreateSteps; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang.StringUtils; -import org.junit.platform.engine.TestExecutionResult; -import org.junit.platform.engine.TestSource; -import org.junit.platform.engine.support.descriptor.MethodSource; -import org.junit.platform.launcher.TestExecutionListener; -import org.junit.platform.launcher.TestIdentifier; - -import javax.annotation.Nullable; -import java.lang.reflect.Method; -import java.text.SimpleDateFormat; -import java.time.Duration; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -import static io.qase.api.QaseClient.getConfig; -import static io.qase.api.utils.IntegrationUtils.getCaseId; -import static io.qase.api.utils.IntegrationUtils.getStacktrace; -import static org.junit.platform.engine.TestExecutionResult.Status.SUCCESSFUL; - -@Slf4j -public class QaseExtension implements TestExecutionListener { - - private final ApiClient apiClient = QaseClient.getApiClient(); - private final ResultsApi resultsApi = new ResultsApi(apiClient); - private final Map testStartTimes = new ConcurrentHashMap<>(); - private static final String QASE_PROJECT = "KAFKAUI"; - - static { - String qaseApiToken = System.getProperty("QASEIO_API_TOKEN"); - if (StringUtils.isEmpty(qaseApiToken)) { - log.warn("QASEIO_API_TOKEN system property is not set. Support for Qase will be disabled."); - System.setProperty("QASE_ENABLE", "false"); - } else { - System.setProperty("QASE_ENABLE", "true"); - System.setProperty("QASE_PROJECT_CODE", QASE_PROJECT); - System.setProperty("QASE_API_TOKEN", qaseApiToken); - System.setProperty("QASE_USE_BULK", "false"); - if ("true".equalsIgnoreCase(System.getProperty("QASEIO_CREATE_TESTRUN"))) { - System.setProperty("QASE_RUN_NAME", "Automation run " + - new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date())); - } - } - } - - @Override - public void executionStarted(TestIdentifier testIdentifier) { - if (QaseClient.isEnabled() && testIdentifier.isTest()) { - testStartTimes.put(testIdentifier, System.currentTimeMillis()); - } - } - - @Override - public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { - if (!testIdentifier.isTest() || !QaseClient.isEnabled() - || !testStartTimes.containsKey(testIdentifier)) { - return; - } - TestSource testSource = testIdentifier.getSource().orElse(null); - Method testMethod = null; - if (testSource instanceof MethodSource) { - testMethod = getMethod((MethodSource) testSource); - } - TestCaseGenerator.createTestCaseIfNotExists(testMethod); - Duration duration = Duration.ofMillis(System.currentTimeMillis() - this.testStartTimes.remove(testIdentifier)); - sendResults(testExecutionResult, duration, testMethod); - } - - private void sendResults(TestExecutionResult testExecutionResult, Duration timeSpent, Method testMethod) { - if (testMethod != null) { - ResultCreate resultCreate = getResultItem(testExecutionResult, timeSpent, testMethod); - try { - resultsApi.createResult(getConfig().projectCode(), - getConfig().runId(), - resultCreate); - log.info("Method = " + testMethod.getName() + ": Result added to test run with Id = {}", getConfig().runId()); - } catch (QaseException e) { - log.error("Method = " + testMethod.getName() + ": Result not added to test Run because there is no @CaseId annotation or case not found", e); - } - } - } - - private ResultCreate getResultItem(TestExecutionResult testExecutionResult, Duration timeSpent, Method testMethod) { - String testCaseTitle = TestCaseGenerator.generateTestCaseTitle(testMethod); - TestCaseGenerator.createTestCaseIfNotExists(testMethod); - Long caseId = getCaseId(testMethod); - Map cases = TestCaseGenerator.getTestCasesTitleAndId(); - StatusEnum status = StatusEnum.SKIPPED; - - if (caseId == null || !TestCaseGenerator.isCaseIdPresentInQaseIo(testMethod)) { - for (Map.Entry map : cases.entrySet()) { - if (map.getValue().matches(testCaseTitle)) { - caseId = map.getKey(); - log.info("There is no annotation @CaseId but there is test case with title '" + testCaseTitle + "' and with id = " + caseId - + " that will be added to test Run"); - } - } - } - - if (TestCaseGenerator.getAutomationStatus(testMethod) == 2) { - status = testExecutionResult.getStatus() == SUCCESSFUL ? StatusEnum.PASSED : StatusEnum.FAILED; - } - - String comment = testExecutionResult.getThrowable() - .flatMap(throwable -> Optional.of(throwable.toString())).orElse(null); - Boolean isDefect = testExecutionResult.getThrowable() - .flatMap(throwable -> Optional.of(throwable instanceof AssertionError)) - .orElse(false); - String stacktrace = testExecutionResult.getThrowable() - .flatMap(throwable -> Optional.of(getStacktrace(throwable))).orElse(null); - LinkedList steps = StepStorage.getSteps(); - return new ResultCreate() - .caseId(caseId) - .status(status) - .timeMs(timeSpent.toMillis()) - .comment(comment) - .stacktrace(stacktrace) - .steps(steps.isEmpty() ? null : steps) - .defect(isDefect); - } - - @Nullable - private Method getMethod(MethodSource testSource) { - try { - Class testClass = Class.forName(testSource.getClassName()); - return Arrays.stream(testClass.getDeclaredMethods()) - .filter(method -> MethodSource.from(method).equals(testSource)) - .findFirst().orElse(null); - } catch (ClassNotFoundException e) { - log.error(e.getMessage()); - return null; - } - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/TestCaseGenerator.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/TestCaseGenerator.java deleted file mode 100644 index 05666621c4..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/TestCaseGenerator.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.provectus.kafka.ui.utilities.qaseIoUtils; - -import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.AutomationStatus; -import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.Suite; -import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status; -import io.qase.api.QaseClient; -import io.qase.api.annotation.CaseId; -import io.qase.client.ApiClient; -import io.qase.client.api.CasesApi; -import io.qase.client.model.*; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; -import org.junit.Assert; -import org.junit.platform.engine.support.descriptor.MethodSource; - -import java.lang.reflect.Method; -import java.util.*; - -import static io.qase.api.QaseClient.getConfig; - -@Slf4j -public class TestCaseGenerator { - - public static boolean FAILED = false; - private static final ApiClient apiClient = QaseClient.getApiClient(); - private static final CasesApi casesApi = new CasesApi(apiClient); - - @SneakyThrows - public static void createTestCaseIfNotExists(Method testMethod) { - TestCaseCreate caseCreate = new TestCaseCreate(); - String testCaseTitle = generateTestCaseTitle(testMethod); - if (!isMethodAnnotatedWithCaseId(testMethod) || !TestCaseGenerator.isCaseIdPresentInQaseIo(testMethod)) { - if (!isCaseTitleExistInQaseIo(testMethod)) { - caseCreate.setTitle(testCaseTitle); - caseCreate.setAutomation(getAutomationStatus(testMethod)); - if (isMethodAnnotatedWithSuite(testMethod)) { - long suiteId = testMethod.getAnnotation(Suite.class).suiteId(); - caseCreate.suiteId(suiteId); - } - Long caseId = Objects.requireNonNull(casesApi.createCase(getConfig().projectCode(), caseCreate).getResult()).getId(); - log.info("New test case = '" + testCaseTitle + "' created with id " + caseId); - } - } - } - - @SneakyThrows - public static HashMap getTestCasesTitleAndId() { - HashMap map = new HashMap<>(); - boolean getCases = true; - int offSet = 0; - while (getCases) { - getCases = false; - TestCaseListResponse response = - casesApi.getCases(getConfig().projectCode(), new Filters().status(Filters.SERIALIZED_NAME_STATUS), 100, offSet); - TestCaseListResponseAllOfResult result = response.getResult(); - Assert.assertNotNull(result); - List entities = result.getEntities(); - Assert.assertNotNull(entities); - if (entities.size() > 0) { - for (TestCase test : entities) { - map.put(test.getId(), test.getTitle()); - } - offSet = offSet + 100; - getCases = true; - } - } - return map; - } - - public static boolean isCaseIdPresentInQaseIo(Method testMethod) { - if (!testMethod.isAnnotationPresent(CaseId.class)) { - return false; - } - long caseId = testMethod.getAnnotation(CaseId.class).value(); - HashMap cases = getTestCasesTitleAndId(); - String title; - if (!cases.containsKey(caseId)) { - FAILED = true; - log.error("The method " + testMethod.getName() + " has wrong @CaseId =" + caseId + " that does not exist in Qase.io. " + - "Please put correct @CaseId"); - return false; - } else { - for (Map.Entry map : cases.entrySet()) { - if (map.getKey().equals(caseId)) { - title = map.getValue(); - if (!title.matches(generateTestCaseTitle(testMethod))) { - log.error("This CaseId =" + caseId + " belong to test with title = " + title); - return false; - } - } - } - } - return true; - } - - private static boolean isCaseTitleExistInQaseIo(Method testMethod) { - HashMap cases = getTestCasesTitleAndId(); - String title = generateTestCaseTitle(testMethod); - if (cases.containsValue(title)) { - for (Map.Entry map : cases.entrySet()) { - if (map.getValue().matches(title)) { - long caseId = map.getKey(); - log.info("Test case with title '" + title + "' and id " + caseId + " exist in Qase.io. Verify that annotation @CaseId is correct"); - return true; - } - } - } - return false; - } - - public static int getAutomationStatus(Method testMethod) { - if (testMethod.isAnnotationPresent(AutomationStatus.class)) { - if (testMethod.getAnnotation(AutomationStatus.class).status().equals(Status.TO_BE_AUTOMATED)) - return 1; - else if (testMethod.getAnnotation(AutomationStatus.class).status().equals(Status.MANUAL)) - return 0; - } - return 2; - } - - private static boolean isMethodAnnotatedWithCaseId(Method testMethod) { - if (!testMethod.isAnnotationPresent(CaseId.class)) { - FAILED = true; - log.error("You must put annotation @CaseId. The method " + testMethod.getName() + " is NOT annotated with @CaseId."); - return false; - } - return true; - } - - private static boolean isMethodAnnotatedWithSuite(Method testMethod) { - if (!testMethod.isAnnotationPresent(Suite.class)) { - log.info("The method " + testMethod.getName() + " is not annotated with @Suite and new test case will be added without suite"); - return false; - } - log.trace("The method is annotated with @Suite with id " + testMethod.getAnnotation(Suite.class).suiteId()); - return true; - } - - private static boolean isMethodAnnotatedWithAutomationStatus(Method testMethod) { - if (!testMethod.isAnnotationPresent(AutomationStatus.class)) { - log.error("The method " + testMethod.getName() + " is NOT annotated with @AutomationStatus."); - return false; - } - return true; - } - - private static String formatTestCaseTitle(String testMethodName) { - String[] split = StringUtils.splitByCharacterTypeCamelCase(testMethodName); - String[] name = Arrays.stream(split).map(String::toLowerCase).toArray(String[]::new); - String[] subarray = ArrayUtils.subarray(name, 1, name.length); - ArrayList stringList = new ArrayList<>(Arrays.asList(subarray)); - stringList.add(0, StringUtils.capitalize(name[0])); - return StringUtils.join(stringList, " "); - } - - public static String generateTestCaseTitle(Method testMethod) { - return getClassName(MethodSource.from(testMethod)) + "." + testMethod.getName() + " : " + - formatTestCaseTitle(testMethod.getName()); - } - - private static String getClassName(MethodSource testSource) { - Class testClass; - try { - testClass = Class.forName(testSource.getClassName()); - } catch (ClassNotFoundException e) { - log.error(e.getMessage()); - return null; - } - return testClass.getSimpleName(); - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/annotations/AutomationStatus.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/annotations/AutomationStatus.java deleted file mode 100644 index abaed2a0ee..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/annotations/AutomationStatus.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.provectus.kafka.ui.utilities.qaseIoUtils.annotations; - -import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Retention(RetentionPolicy.RUNTIME) -public @interface AutomationStatus { - - Status status(); -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/annotations/Suite.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/annotations/Suite.java deleted file mode 100644 index 3e688cd797..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/annotations/Suite.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.provectus.kafka.ui.utilities.qaseIoUtils.annotations; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Retention(RetentionPolicy.RUNTIME) -public @interface Suite { - long suiteId(); - String title(); -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/enums/Status.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/enums/Status.java deleted file mode 100644 index 024acbc91a..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseIoUtils/enums/Status.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.provectus.kafka.ui.utilities.qaseIoUtils.enums; - -public enum Status { - AUTOMATED, TO_BE_AUTOMATED, MANUAL; -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/QaseSetup.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/QaseSetup.java new file mode 100644 index 0000000000..60be014a0a --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/QaseSetup.java @@ -0,0 +1,34 @@ +package com.provectus.kafka.ui.utilities.qaseUtils; + +import lombok.extern.slf4j.Slf4j; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +import static com.provectus.kafka.ui.settings.BaseSource.SUITE_NAME; +import static com.provectus.kafka.ui.variables.Suite.MANUAL; +import static org.apache.commons.lang3.BooleanUtils.FALSE; +import static org.apache.commons.lang3.BooleanUtils.TRUE; +import static org.apache.commons.lang3.StringUtils.isEmpty; + +@Slf4j +public class QaseSetup { + + public static void qaseIntegrationSetup() { + String qaseApiToken = System.getProperty("QASEIO_API_TOKEN"); + if (isEmpty(qaseApiToken)) { + log.warn("Integration with Qase is disabled due to run config or token wasn't defined."); + System.setProperty("QASE_ENABLE", FALSE); + } else { + log.warn("Integration with Qase is enabled. Find this run at https://app.qase.io/run/KAFKAUI."); + String automation = SUITE_NAME.equalsIgnoreCase(MANUAL) ? "" : "Automation "; + System.setProperty("QASE_ENABLE", TRUE); + System.setProperty("QASE_PROJECT_CODE", "KAFKAUI"); + System.setProperty("QASE_API_TOKEN", qaseApiToken); + System.setProperty("QASE_USE_BULK", TRUE); + System.setProperty("QASE_RUN_NAME", DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm") + .format(OffsetDateTime.now(ZoneOffset.UTC)) + ": " + automation + SUITE_NAME.toUpperCase() + " suite"); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Automation.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Automation.java new file mode 100644 index 0000000000..556263c111 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Automation.java @@ -0,0 +1,15 @@ +package com.provectus.kafka.ui.utilities.qaseUtils.annotations; + +import com.provectus.kafka.ui.utilities.qaseUtils.enums.State; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Automation { + + State state(); +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Status.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Status.java new file mode 100644 index 0000000000..3c31f2345c --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Status.java @@ -0,0 +1,13 @@ +package com.provectus.kafka.ui.utilities.qaseUtils.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Status { + + com.provectus.kafka.ui.utilities.qaseUtils.enums.Status status(); +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Suite.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Suite.java new file mode 100644 index 0000000000..fa1c2c3dd7 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Suite.java @@ -0,0 +1,13 @@ +package com.provectus.kafka.ui.utilities.qaseUtils.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Suite { + + long id(); +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/enums/State.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/enums/State.java new file mode 100644 index 0000000000..cdbbaf38de --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/enums/State.java @@ -0,0 +1,18 @@ +package com.provectus.kafka.ui.utilities.qaseUtils.enums; + +public enum State { + + NOT_AUTOMATED(0), + TO_BE_AUTOMATED(1), + AUTOMATED(2); + + private final int value; + + State(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/enums/Status.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/enums/Status.java new file mode 100644 index 0000000000..adc8bf24b9 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/enums/Status.java @@ -0,0 +1,18 @@ +package com.provectus.kafka.ui.utilities.qaseUtils.enums; + +public enum Status { + + ACTUAL(0), + DRAFT(1), + DEPRECATED(2); + + private final int value; + + Status(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Browser.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Browser.java new file mode 100644 index 0000000000..cb3873cdb5 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Browser.java @@ -0,0 +1,7 @@ +package com.provectus.kafka.ui.variables; + +public interface Browser { + + String CONTAINER = "container"; + String LOCAL = "local"; +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Suite.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Suite.java new file mode 100644 index 0000000000..74f60dd0d2 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Suite.java @@ -0,0 +1,10 @@ +package com.provectus.kafka.ui.variables; + +public interface Suite { + + String CUSTOM = "custom"; + String MANUAL = "manual"; + String REGRESSION = "regression"; + String SANITY = "sanity"; + String SMOKE = "smoke"; +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Url.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Url.java new file mode 100644 index 0000000000..f612d743a5 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Url.java @@ -0,0 +1,11 @@ +package com.provectus.kafka.ui.variables; + +public interface Url { + + String BROKERS_LIST_URL = "http://%s:8080/ui/clusters/local/brokers"; + String TOPICS_LIST_URL = "http://%s:8080/ui/clusters/local/all-topics?perPage=25"; + String CONSUMERS_LIST_URL = "http://%s:8080/ui/clusters/local/consumer-groups"; + String SCHEMA_REGISTRY_LIST_URL = "http://%s:8080/ui/clusters/local/schemas"; + String KAFKA_CONNECT_LIST_URL = "http://%s:8080/ui/clusters/local/connectors"; + String KSQL_DB_LIST_URL = "http://%s:8080/ui/clusters/local/ksqldb/tables"; +} diff --git a/kafka-ui-e2e-checks/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener b/kafka-ui-e2e-checks/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener deleted file mode 100644 index dd3c3f5e90..0000000000 --- a/kafka-ui-e2e-checks/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener +++ /dev/null @@ -1 +0,0 @@ -com.provectus.kafka.ui.utilities.qaseIoUtils.QaseExtension \ No newline at end of file diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/BaseTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/BaseTest.java new file mode 100644 index 0000000000..b2e7e007e5 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/BaseTest.java @@ -0,0 +1,196 @@ +package com.provectus.kafka.ui; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.Selenide; +import com.codeborne.selenide.SelenideElement; +import com.codeborne.selenide.WebDriverRunner; +import com.provectus.kafka.ui.settings.listeners.AllureListener; +import com.provectus.kafka.ui.settings.listeners.LoggerListener; +import com.provectus.kafka.ui.settings.listeners.QaseResultListener; +import io.qameta.allure.Step; +import lombok.extern.slf4j.Slf4j; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.testcontainers.Testcontainers; +import org.testcontainers.containers.BrowserWebDriverContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.utility.DockerImageName; +import org.testng.annotations.*; +import org.testng.asserts.SoftAssert; + +import java.time.Duration; +import java.util.List; + +import static com.provectus.kafka.ui.pages.NaviSideBar.SideMenuOption.*; +import static com.provectus.kafka.ui.settings.BaseSource.*; +import static com.provectus.kafka.ui.settings.drivers.LocalWebDriver.*; +import static com.provectus.kafka.ui.utilities.qaseUtils.QaseSetup.qaseIntegrationSetup; +import static com.provectus.kafka.ui.variables.Browser.CONTAINER; +import static com.provectus.kafka.ui.variables.Browser.LOCAL; + +@Slf4j +@Listeners({AllureListener.class, LoggerListener.class, QaseResultListener.class}) +public abstract class BaseTest extends Facade { + + private static final String SELENIUM_IMAGE_NAME = "selenium/standalone-chrome:103.0"; + private static final String SELENIARM_STANDALONE_CHROMIUM = "seleniarm/standalone-chromium:103.0"; + protected static BrowserWebDriverContainer webDriverContainer = null; + + private static boolean isARM64() { + return System.getProperty("os.arch").equals("aarch64"); + } + + @BeforeSuite(alwaysRun = true) + public void beforeSuite() { + qaseIntegrationSetup(); + switch (BROWSER) { + case (CONTAINER) -> { + DockerImageName image = isARM64() + ? DockerImageName.parse(SELENIARM_STANDALONE_CHROMIUM).asCompatibleSubstituteFor(SELENIUM_IMAGE_NAME) + : DockerImageName.parse(SELENIUM_IMAGE_NAME); + log.info("Using [{}] as image name for chrome", image.getUnversionedPart()); + webDriverContainer = new BrowserWebDriverContainer<>(image) + .withEnv("JAVA_OPTS", "-Dwebdriver.chrome.whitelistedIps=") + .withStartupTimeout(Duration.ofSeconds(180)) + .withCapabilities(new ChromeOptions() + .addArguments("--disable-dev-shm-usage") + .addArguments("--disable-gpu") + .addArguments("--no-sandbox") + .addArguments("--verbose") + .addArguments("--lang=es") + ) + .withLogConsumer(new Slf4jLogConsumer(log).withPrefix("[CHROME]: ")); + try { + Testcontainers.exposeHostPorts(8080); + log.info("Starting browser container"); + webDriverContainer.start(); + } catch (Throwable e) { + log.error("Couldn't start a container", e); + } + } + case (LOCAL) -> loggerSetup(); + default -> throw new IllegalStateException("Unexpected value: " + BROWSER); + } + } + + @AfterSuite(alwaysRun = true) + public void afterSuite() { + switch (BROWSER) { + case (CONTAINER) -> { + if (webDriverContainer.isRunning()) { + webDriverContainer.close(); + webDriverContainer.stop(); + } + } + case (LOCAL) -> browserQuit(); + default -> throw new IllegalStateException("Unexpected value: " + BROWSER); + } + } + + @BeforeMethod(alwaysRun = true) + public void beforeMethod() { + switch (BROWSER) { + case (CONTAINER) -> { + RemoteWebDriver remoteWebDriver = webDriverContainer.getWebDriver(); + WebDriverRunner.setWebDriver(remoteWebDriver); + remoteWebDriver.manage() + .window().setSize(new Dimension(1440, 1024)); + Selenide.open(BASE_CONTAINER_URL); + } + case (LOCAL) -> openUrl(BASE_LOCAL_URL); + default -> throw new IllegalStateException("Unexpected value: " + BROWSER); + } + naviSideBar.waitUntilScreenReady(); + } + + @AfterMethod(alwaysRun = true) + public void afterMethod() { + browserClear(); + } + + @Step + protected void navigateToBrokers() { + naviSideBar + .openSideMenu(BROKERS); + brokersList + .waitUntilScreenReady(); + } + + @Step + protected void navigateToTopics() { + naviSideBar + .openSideMenu(TOPICS); + topicsList + .waitUntilScreenReady(); + } + + @Step + protected void navigateToTopicsAndOpenDetails(String topicName) { + naviSideBar + .openSideMenu(TOPICS); + topicsList + .waitUntilScreenReady() + .openTopic(topicName); + topicDetails + .waitUntilScreenReady(); + } + + @Step + protected void navigateToConsumers() { + naviSideBar + .openSideMenu(CONSUMERS); + consumersList + .waitUntilScreenReady(); + } + + @Step + protected void navigateToSchemaRegistry() { + naviSideBar + .openSideMenu(SCHEMA_REGISTRY); + schemaRegistryList + .waitUntilScreenReady(); + } + + @Step + protected void navigateToSchemaRegistryAndOpenDetails(String schemaName) { + navigateToSchemaRegistry(); + schemaRegistryList + .openSchema(schemaName); + schemaDetails + .waitUntilScreenReady(); + } + + @Step + protected void navigateToConnectors() { + naviSideBar + .openSideMenu(KAFKA_CONNECT); + kafkaConnectList + .waitUntilScreenReady(); + } + + @Step + protected void navigateToConnectorsAndOpenDetails(String connectorName) { + navigateToConnectors(); + kafkaConnectList + .openConnector(connectorName); + connectorDetails + .waitUntilScreenReady(); + } + + @Step + protected void navigateToKsqlDb() { + naviSideBar + .openSideMenu(KSQL_DB); + ksqlDbList + .waitUntilScreenReady(); + } + + @Step + protected void verifyElementsCondition(List elementList, Condition expectedCondition) { + SoftAssert softly = new SoftAssert(); + elementList.forEach(element -> softly.assertTrue(element.is(expectedCondition), + element.getSearchCriteria() + " is " + expectedCondition)); + softly.assertAll(); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/Facade.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/Facade.java similarity index 66% rename from kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/Facade.java rename to kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/Facade.java index 1dad47a700..c370c01b5f 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/Facade.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/Facade.java @@ -1,26 +1,25 @@ -package com.provectus.kafka.ui.base; +package com.provectus.kafka.ui; -import com.provectus.kafka.ui.services.ApiService; import com.provectus.kafka.ui.pages.NaviSideBar; import com.provectus.kafka.ui.pages.TopPanel; import com.provectus.kafka.ui.pages.brokers.BrokersConfigTab; import com.provectus.kafka.ui.pages.brokers.BrokersDetails; import com.provectus.kafka.ui.pages.brokers.BrokersList; -import com.provectus.kafka.ui.pages.connector.ConnectorCreateForm; -import com.provectus.kafka.ui.pages.connector.ConnectorDetails; -import com.provectus.kafka.ui.pages.connector.KafkaConnectList; -import com.provectus.kafka.ui.pages.consumer.ConsumersDetails; -import com.provectus.kafka.ui.pages.consumer.ConsumersList; -import com.provectus.kafka.ui.pages.schema.SchemaCreateForm; -import com.provectus.kafka.ui.pages.schema.SchemaDetails; -import com.provectus.kafka.ui.pages.schema.SchemaRegistryList; -import com.provectus.kafka.ui.pages.topic.ProduceMessagePanel; -import com.provectus.kafka.ui.pages.topic.TopicSettingsTab; -import com.provectus.kafka.ui.pages.topic.TopicCreateEditForm; -import com.provectus.kafka.ui.pages.topic.TopicDetails; -import com.provectus.kafka.ui.pages.topic.TopicsList; +import com.provectus.kafka.ui.pages.connectors.ConnectorCreateForm; +import com.provectus.kafka.ui.pages.connectors.ConnectorDetails; +import com.provectus.kafka.ui.pages.connectors.KafkaConnectList; +import com.provectus.kafka.ui.pages.consumers.ConsumersDetails; +import com.provectus.kafka.ui.pages.consumers.ConsumersList; +import com.provectus.kafka.ui.pages.ksqlDb.KsqlDbList; +import com.provectus.kafka.ui.pages.ksqlDb.KsqlQueryForm; +import com.provectus.kafka.ui.pages.schemas.SchemaCreateForm; +import com.provectus.kafka.ui.pages.schemas.SchemaDetails; +import com.provectus.kafka.ui.pages.schemas.SchemaRegistryList; +import com.provectus.kafka.ui.pages.topics.*; +import com.provectus.kafka.ui.services.ApiService; public abstract class Facade { + protected ApiService apiService = new ApiService(); protected ConnectorCreateForm connectorCreateForm = new ConnectorCreateForm(); protected KafkaConnectList kafkaConnectList = new KafkaConnectList(); @@ -40,4 +39,6 @@ public abstract class Facade { protected BrokersDetails brokersDetails = new BrokersDetails(); protected BrokersConfigTab brokersConfigTab = new BrokersConfigTab(); protected TopicSettingsTab topicSettingsTab = new TopicSettingsTab(); + protected KsqlQueryForm ksqlQueryForm = new KsqlQueryForm(); + protected KsqlDbList ksqlDbList = new KsqlDbList(); } diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/BaseTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/BaseTest.java deleted file mode 100644 index 6db92244ba..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/BaseTest.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.provectus.kafka.ui.base; - -import static com.provectus.kafka.ui.pages.NaviSideBar.SideMenuOption.TOPICS; -import static com.provectus.kafka.ui.settings.BaseSource.BASE_CONTAINER_URL; -import static com.provectus.kafka.ui.settings.BaseSource.BASE_LOCAL_URL; -import static com.provectus.kafka.ui.settings.BaseSource.BROWSER; -import static com.provectus.kafka.ui.settings.configs.Profiles.CONTAINER; -import static com.provectus.kafka.ui.settings.configs.Profiles.LOCAL; -import static com.provectus.kafka.ui.settings.drivers.LocalWebDriver.browserClear; -import static com.provectus.kafka.ui.settings.drivers.LocalWebDriver.browserQuit; -import static com.provectus.kafka.ui.settings.drivers.LocalWebDriver.loggerSetup; -import static com.provectus.kafka.ui.settings.drivers.LocalWebDriver.openUrl; - -import com.codeborne.selenide.Condition; -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; -import com.codeborne.selenide.WebDriverRunner; -import com.provectus.kafka.ui.utilities.qaseIoUtils.DisplayNameGenerator; -import io.qase.api.annotation.Step; -import java.time.Duration; -import java.util.List; -import lombok.extern.slf4j.Slf4j; -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.openqa.selenium.Dimension; -import org.openqa.selenium.chrome.ChromeOptions; -import org.openqa.selenium.remote.RemoteWebDriver; -import org.testcontainers.Testcontainers; -import org.testcontainers.containers.BrowserWebDriverContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.utility.DockerImageName; - -@Slf4j -@DisplayNameGeneration(DisplayNameGenerator.class) -public abstract class BaseTest extends Facade { - - private static final String SELENIUM_IMAGE_NAME = "selenium/standalone-chrome:103.0"; - private static final String SELENIARM_STANDALONE_CHROMIUM = "seleniarm/standalone-chromium:103.0"; - protected static BrowserWebDriverContainer webDriverContainer = null; - - private static boolean isARM64() { - return System.getProperty("os.arch").equals("aarch64"); - } - - @BeforeAll - public static void start() { - switch (BROWSER) { - case (CONTAINER) -> { - DockerImageName image = isARM64() - ? DockerImageName.parse(SELENIARM_STANDALONE_CHROMIUM).asCompatibleSubstituteFor(SELENIUM_IMAGE_NAME) - : DockerImageName.parse(SELENIUM_IMAGE_NAME); - log.info("Using [{}] as image name for chrome", image.getUnversionedPart()); - webDriverContainer = new BrowserWebDriverContainer<>(image) - .withEnv("JAVA_OPTS", "-Dwebdriver.chrome.whitelistedIps=") - .withStartupTimeout(Duration.ofSeconds(180)) - .withCapabilities(new ChromeOptions() - .addArguments("--disable-dev-shm-usage") - .addArguments("--disable-gpu") - .addArguments("--no-sandbox") - .addArguments("--verbose") - .addArguments("--lang=es") - ) - .withLogConsumer(new Slf4jLogConsumer(log).withPrefix("[CHROME]: ")); - try { - Testcontainers.exposeHostPorts(8080); - log.info("Starting browser container"); - webDriverContainer.start(); - } catch (Throwable e) { - log.error("Couldn't start a container", e); - } - } - case (LOCAL) -> loggerSetup(); - default -> throw new IllegalStateException("Unexpected value: " + BROWSER); - } - } - - @AfterAll - public static void tearDown() { - switch (BROWSER) { - case (CONTAINER) -> { - if (webDriverContainer.isRunning()) { - webDriverContainer.close(); - webDriverContainer.stop(); - } - } - case (LOCAL) -> browserQuit(); - default -> throw new IllegalStateException("Unexpected value: " + BROWSER); - } - } - - @BeforeEach - public void beforeMethod() { - switch (BROWSER) { - case (CONTAINER) -> { - RemoteWebDriver remoteWebDriver = webDriverContainer.getWebDriver(); - WebDriverRunner.setWebDriver(remoteWebDriver); - remoteWebDriver.manage() - .window().setSize(new Dimension(1440, 1024)); - Selenide.open(BASE_CONTAINER_URL); - } - case (LOCAL) -> openUrl(BASE_LOCAL_URL); - default -> throw new IllegalStateException("Unexpected value: " + BROWSER); - } - naviSideBar.waitUntilScreenReady(); - } - - @AfterEach - public void afterMethod() { - browserClear(); - } - - @Step - protected void navigateToTopics() { - naviSideBar - .openSideMenu(TOPICS); - topicsList - .waitUntilScreenReady(); - } - - @Step - protected void navigateToTopicsAndOpenDetails(String topicName){ - naviSideBar - .openSideMenu(TOPICS); - topicsList - .waitUntilScreenReady() - .openTopic(topicName); - topicDetails - .waitUntilScreenReady(); - } - - @Step - protected void verifyElementsCondition(List elementList, Condition expectedCondition) { - SoftAssertions softly = new SoftAssertions(); - elementList.forEach(element -> softly.assertThat(element.is(expectedCondition)) - .as(element.getSearchCriteria() + " is " + expectedCondition).isTrue()); - softly.assertAll(); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/BaseManualTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/BaseManualTest.java new file mode 100644 index 0000000000..d9891a0b50 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/BaseManualTest.java @@ -0,0 +1,28 @@ +package com.provectus.kafka.ui.manualSuite; + +import com.provectus.kafka.ui.settings.listeners.QaseResultListener; +import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; +import org.testng.SkipException; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Listeners; + +import java.lang.reflect.Method; + +import static com.provectus.kafka.ui.utilities.qaseUtils.QaseSetup.qaseIntegrationSetup; +import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.NOT_AUTOMATED; + +@Listeners(QaseResultListener.class) +public abstract class BaseManualTest { + + @BeforeSuite + public void beforeSuite() { + qaseIntegrationSetup(); + } + + @BeforeMethod + public void beforeMethod(Method method) { + if (method.getAnnotation(Automation.class).state().equals(NOT_AUTOMATED)) + throw new SkipException("Skip test exception"); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/BrokersTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/BrokersTest.java new file mode 100644 index 0000000000..eb31d0c2b1 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/BrokersTest.java @@ -0,0 +1,17 @@ +package com.provectus.kafka.ui.manualSuite.suite; + +import com.provectus.kafka.ui.manualSuite.BaseManualTest; +import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.Test; + +import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.TO_BE_AUTOMATED; + +public class BrokersTest extends BaseManualTest { + + @Automation(state = TO_BE_AUTOMATED) + @QaseId(330) + @Test + public void testCaseA() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/DataMaskingTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/DataMaskingTest.java new file mode 100644 index 0000000000..23b6e6539e --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/DataMaskingTest.java @@ -0,0 +1,29 @@ +package com.provectus.kafka.ui.manualSuite.suite; + +import com.provectus.kafka.ui.manualSuite.BaseManualTest; +import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.Test; + +import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.NOT_AUTOMATED; + +public class DataMaskingTest extends BaseManualTest { + + @Automation(state = NOT_AUTOMATED) + @QaseId(262) + @Test + public void testCaseA() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(264) + @Test + public void testCaseB() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(265) + @Test + public void testCaseC() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/KsqlDbTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/KsqlDbTest.java new file mode 100644 index 0000000000..443fc85ef2 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/KsqlDbTest.java @@ -0,0 +1,35 @@ +package com.provectus.kafka.ui.manualSuite.suite; + +import com.provectus.kafka.ui.manualSuite.BaseManualTest; +import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.Test; + +import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.TO_BE_AUTOMATED; + +public class KsqlDbTest extends BaseManualTest { + + @Automation(state = TO_BE_AUTOMATED) + @QaseId(276) + @Test + public void testCaseA() { + } + + @Automation(state = TO_BE_AUTOMATED) + @QaseId(277) + @Test + public void testCaseB() { + } + + @Automation(state = TO_BE_AUTOMATED) + @QaseId(278) + @Test + public void testCaseC() { + } + + @Automation(state = TO_BE_AUTOMATED) + @QaseId(284) + @Test + public void testCaseD() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/RbacTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/RbacTest.java new file mode 100644 index 0000000000..aad85652f5 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/RbacTest.java @@ -0,0 +1,53 @@ +package com.provectus.kafka.ui.manualSuite.suite; + +import com.provectus.kafka.ui.manualSuite.BaseManualTest; +import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.Test; + +import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.NOT_AUTOMATED; + +public class RbacTest extends BaseManualTest { + + @Automation(state = NOT_AUTOMATED) + @QaseId(249) + @Test + public void testCaseA() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(251) + @Test + public void testCaseB() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(257) + @Test + public void testCaseC() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(258) + @Test + public void testCaseD() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(259) + @Test + public void testCaseE() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(260) + @Test + public void testCaseF() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(261) + @Test + public void testCaseG() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/TopicsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/TopicsTest.java new file mode 100644 index 0000000000..35188e3f45 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/TopicsTest.java @@ -0,0 +1,101 @@ +package com.provectus.kafka.ui.manualSuite.suite; + +import com.provectus.kafka.ui.manualSuite.BaseManualTest; +import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.Test; + +import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.NOT_AUTOMATED; + +public class TopicsTest extends BaseManualTest { + + @Automation(state = NOT_AUTOMATED) + @QaseId(17) + @Test + public void testCaseA() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(18) + @Test + public void testCaseB() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(21) + @Test() + public void testCaseC() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(22) + @Test + public void testCaseD() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(46) + @Test + public void testCaseE() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(47) + @Test + public void testCaseF() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(48) + @Test + public void testCaseG() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(49) + @Test + public void testCaseH() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(50) + @Test + public void testCaseI() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(57) + @Test + public void testCaseJ() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(58) + @Test + public void testCaseK() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(269) + @Test + public void testCaseL() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(270) + @Test + public void testCaseM() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(271) + @Test + public void testCaseN() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(272) + @Test + public void testCaseO() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/BaseQaseTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/BaseQaseTest.java new file mode 100644 index 0000000000..977cbd6dc4 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/BaseQaseTest.java @@ -0,0 +1,16 @@ +package com.provectus.kafka.ui.qaseSuite; + +import com.provectus.kafka.ui.settings.listeners.QaseCreateListener; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Listeners; + +import static com.provectus.kafka.ui.utilities.qaseUtils.QaseSetup.qaseIntegrationSetup; + +@Listeners(QaseCreateListener.class) +public abstract class BaseQaseTest { + + @BeforeSuite + public void beforeSuite() { + qaseIntegrationSetup(); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/Template.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/Template.java new file mode 100644 index 0000000000..cf0101fe79 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/Template.java @@ -0,0 +1,58 @@ +package com.provectus.kafka.ui.qaseSuite; + +import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; +import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Status; +import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Suite; +import io.qase.api.annotation.QaseTitle; +import io.qase.api.annotation.Step; + +import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.NOT_AUTOMATED; +import static com.provectus.kafka.ui.utilities.qaseUtils.enums.Status.DRAFT; + +public class Template extends BaseQaseTest { + + /** + * this class is a kind of placeholder or example, use is as template to create new one + * copy class into kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/suite + * place it into regarding folder and rename according to test case summary from Qase.io + * uncomment @Test and set all annotations according to kafka-ui-e2e-checks/QASE.md + */ + + @Automation(state = NOT_AUTOMATED) + @QaseTitle("testCaseA title") + @Status(status = DRAFT) + @Suite(id = 0) +// @org.testng.annotations.Test + public void testCaseA() { + stepA(); + stepB(); + stepC(); + stepD(); + stepE(); + stepF(); + } + + @Step("stepA action") + private void stepA() { + } + + @Step("stepB action") + private void stepB() { + } + + @Step("stepC action") + private void stepC() { + } + + @Step("stepD action") + private void stepD() { + } + + @Step("stepE action") + private void stepE() { + } + + @Step("stepF action") + private void stepF() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/sanitySuite/TestClass.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/sanitySuite/TestClass.java new file mode 100644 index 0000000000..89402f3fc8 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/sanitySuite/TestClass.java @@ -0,0 +1,4 @@ +package com.provectus.kafka.ui.sanitySuite; + +public class TestClass { +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/SmokeTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/SmokeTest.java new file mode 100644 index 0000000000..0ea5d2eb82 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/SmokeTest.java @@ -0,0 +1,54 @@ +package com.provectus.kafka.ui.smokeSuite; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.WebDriverRunner; +import com.provectus.kafka.ui.BaseTest; +import io.qameta.allure.Step; +import io.qase.api.annotation.QaseId; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.provectus.kafka.ui.settings.BaseSource.BROWSER; +import static com.provectus.kafka.ui.variables.Browser.LOCAL; +import static com.provectus.kafka.ui.variables.Url.*; + +public class SmokeTest extends BaseTest { + + @QaseId(198) + @Test + public void checkBasePageElements() { + verifyElementsCondition( + Stream.concat(topPanel.getAllVisibleElements().stream(), naviSideBar.getAllMenuButtons().stream()) + .collect(Collectors.toList()), Condition.visible); + verifyElementsCondition( + Stream.concat(topPanel.getAllEnabledElements().stream(), naviSideBar.getAllMenuButtons().stream()) + .collect(Collectors.toList()), Condition.enabled); + } + + @QaseId(45) + @Test + public void checkUrlWhileNavigating() { + navigateToBrokers(); + verifyCurrentUrl(BROKERS_LIST_URL); + navigateToTopics(); + verifyCurrentUrl(TOPICS_LIST_URL); + navigateToConsumers(); + verifyCurrentUrl(CONSUMERS_LIST_URL); + navigateToSchemaRegistry(); + verifyCurrentUrl(SCHEMA_REGISTRY_LIST_URL); + navigateToConnectors(); + verifyCurrentUrl(KAFKA_CONNECT_LIST_URL); + navigateToKsqlDb(); + verifyCurrentUrl(KSQL_DB_LIST_URL); + } + + @Step + private void verifyCurrentUrl(String expectedUrl) { + String host = BROWSER.equals(LOCAL) ? "localhost" : "host.testcontainers.internal"; + Assert.assertEquals(WebDriverRunner.getWebDriver().getCurrentUrl(), + String.format(expectedUrl, host), "getCurrentUrl()"); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/brokers/BrokersTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/brokers/BrokersTest.java new file mode 100644 index 0000000000..c9029e30ae --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/brokers/BrokersTest.java @@ -0,0 +1,41 @@ +package com.provectus.kafka.ui.smokeSuite.brokers; + +import com.codeborne.selenide.Condition; +import com.provectus.kafka.ui.BaseTest; +import io.qase.api.annotation.QaseId; +import org.testng.Assert; +import org.testng.annotations.Test; + +import static com.provectus.kafka.ui.pages.brokers.BrokersDetails.DetailsTab.CONFIGS; + +public class BrokersTest extends BaseTest { + + @QaseId(1) + @Test + public void checkBrokersOverview() { + navigateToBrokers(); + Assert.assertTrue(brokersList.getAllBrokers().size() > 0, "getAllBrokers()"); + verifyElementsCondition(brokersList.getAllVisibleElements(), Condition.visible); + verifyElementsCondition(brokersList.getAllEnabledElements(), Condition.enabled); + } + + @QaseId(85) + @Test + public void checkExistingBrokersInCluster() { + navigateToBrokers(); + Assert.assertTrue(brokersList.getAllBrokers().size() > 0, "getAllBrokers()"); + brokersList + .openBroker(1); + brokersDetails + .waitUntilScreenReady(); + verifyElementsCondition(brokersDetails.getAllVisibleElements(), Condition.visible); + verifyElementsCondition(brokersDetails.getAllEnabledElements(), Condition.enabled); + brokersDetails + .openDetailsTab(CONFIGS); + brokersConfigTab + .waitUntilScreenReady(); + verifyElementsCondition(brokersConfigTab.getColumnHeaders(), Condition.visible); + verifyElementsCondition(brokersConfigTab.getEditButtons(), Condition.enabled); + Assert.assertTrue(brokersConfigTab.isSearchByKeyVisible(), "isSearchByKeyVisible()"); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/connectors/ConnectorsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/connectors/ConnectorsTest.java new file mode 100644 index 0000000000..c54138d8c5 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/connectors/ConnectorsTest.java @@ -0,0 +1,108 @@ +package com.provectus.kafka.ui.smokeSuite.connectors; + +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.models.Connector; +import com.provectus.kafka.ui.models.Topic; +import io.qase.api.annotation.QaseId; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.List; + +import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; +import static com.provectus.kafka.ui.utilities.FileUtils.getResourceAsString; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + +public class ConnectorsTest extends BaseTest { + + private static final String CONNECT_NAME = "first"; + private static final List TOPIC_LIST = new ArrayList<>(); + private static final List CONNECTOR_LIST = new ArrayList<>(); + private static final String MESSAGE_CONTENT = "message_content_create_topic.json"; + private static final String MESSAGE_KEY = " "; + private static final Topic TOPIC_FOR_CREATE = new Topic() + .setName("topic_for_create_connector-" + randomAlphabetic(5)) + .setMessageContent(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); + private static final Topic TOPIC_FOR_DELETE = new Topic() + .setName("topic_for_delete_connector-" + randomAlphabetic(5)) + .setMessageContent(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); + private static final Topic TOPIC_FOR_UPDATE = new Topic() + .setName("topic_for_update_connector-" + randomAlphabetic(5)) + .setMessageContent(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); + private static final Connector CONNECTOR_FOR_DELETE = new Connector() + .setName("sink_postgres_activities_e2e_checks_for_delete-" + randomAlphabetic(5)) + .setConfig(getResourceAsString("delete_connector_config.json")); + private static final Connector CONNECTOR_FOR_UPDATE = new Connector() + .setName("sink_postgres_activities_e2e_checks_for_update-" + randomAlphabetic(5)) + .setConfig(getResourceAsString("config_for_create_connector_via_api.json")); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + TOPIC_LIST.addAll(List.of(TOPIC_FOR_CREATE, TOPIC_FOR_DELETE, TOPIC_FOR_UPDATE)); + TOPIC_LIST.forEach(topic -> apiService + .createTopic(topic.getName()) + .sendMessage(topic) + ); + CONNECTOR_LIST.addAll(List.of(CONNECTOR_FOR_DELETE, CONNECTOR_FOR_UPDATE)); + CONNECTOR_LIST.forEach(connector -> apiService + .createConnector(CONNECT_NAME, connector)); + } + + @QaseId(42) + @Test + public void createConnector() { + Connector connectorForCreate = new Connector() + .setName("sink_postgres_activities_e2e_checks-" + randomAlphabetic(5)) + .setConfig(getResourceAsString("config_for_create_connector.json")); + navigateToConnectors(); + kafkaConnectList + .clickCreateConnectorBtn(); + connectorCreateForm + .waitUntilScreenReady() + .setConnectorDetails(connectorForCreate.getName(), connectorForCreate.getConfig()) + .clickSubmitButton(); + connectorDetails + .waitUntilScreenReady(); + navigateToConnectorsAndOpenDetails(connectorForCreate.getName()); + Assert.assertTrue(connectorDetails.isConnectorHeaderVisible(connectorForCreate.getName()), "isConnectorTitleVisible()"); + navigateToConnectors(); + Assert.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), "isConnectorVisible()"); + CONNECTOR_LIST.add(connectorForCreate); + } + + @QaseId(196) + @Test + public void updateConnector() { + navigateToConnectorsAndOpenDetails(CONNECTOR_FOR_UPDATE.getName()); + connectorDetails + .openConfigTab() + .setConfig(CONNECTOR_FOR_UPDATE.getConfig()) + .clickSubmitButton(); + Assert.assertTrue(connectorDetails.isAlertWithMessageVisible(SUCCESS, "Config successfully updated."), "isAlertWithMessageVisible()"); + navigateToConnectors(); + Assert.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_UPDATE.getName()), "isConnectorVisible()"); + } + + @QaseId(195) + @Test + public void deleteConnector() { + navigateToConnectorsAndOpenDetails(CONNECTOR_FOR_DELETE.getName()); + connectorDetails + .openDotMenu() + .clickDeleteBtn() + .clickConfirmBtn(); + navigateToConnectors(); + Assert.assertFalse(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), "isConnectorVisible()"); + CONNECTOR_LIST.remove(CONNECTOR_FOR_DELETE); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + CONNECTOR_LIST.forEach(connector -> + apiService.deleteConnector(CONNECT_NAME, connector.getName())); + TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/ksqlDb/KsqlDbTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/ksqlDb/KsqlDbTest.java new file mode 100644 index 0000000000..ab1705922a --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/ksqlDb/KsqlDbTest.java @@ -0,0 +1,68 @@ +package com.provectus.kafka.ui.smokeSuite.ksqlDb; + +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.pages.ksqlDb.models.Stream; +import com.provectus.kafka.ui.pages.ksqlDb.models.Table; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.testng.asserts.SoftAssert; + +import static com.provectus.kafka.ui.pages.ksqlDb.enums.KsqlQueryConfig.SHOW_TABLES; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + +public class KsqlDbTest extends BaseTest { + + private static final Stream STREAM_FOR_CHECK_TABLES = new Stream() + .setName("STREAM_FOR_CHECK_TABLES_" + randomAlphabetic(4).toUpperCase()) + .setTopicName("TOPIC_FOR_STREAM_" + randomAlphabetic(4).toUpperCase()); + private static final Table FIRST_TABLE = new Table() + .setName("FIRST_TABLE" + randomAlphabetic(4).toUpperCase()) + .setStreamName(STREAM_FOR_CHECK_TABLES.getName()); + private static final Table SECOND_TABLE = new Table() + .setName("SECOND_TABLE" + randomAlphabetic(4).toUpperCase()) + .setStreamName(STREAM_FOR_CHECK_TABLES.getName()); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + apiService + .createStream(STREAM_FOR_CHECK_TABLES) + .createTables(FIRST_TABLE, SECOND_TABLE); + } + + @QaseId(41) + @Test(priority = 1) + public void checkShowTablesRequestExecution() { + navigateToKsqlDb(); + ksqlDbList + .clickExecuteKsqlRequestBtn(); + ksqlQueryForm + .waitUntilScreenReady() + .setQuery(SHOW_TABLES.getQuery()) + .clickExecuteBtn(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); + softly.assertTrue(ksqlQueryForm.getTableByName(FIRST_TABLE.getName()).isVisible(), "getTableName()"); + softly.assertTrue(ksqlQueryForm.getTableByName(SECOND_TABLE.getName()).isVisible(), "getTableName()"); + softly.assertAll(); + } + + @QaseId(86) + @Test(priority = 2) + public void clearResultsForExecutedRequest() { + navigateToKsqlDb(); + ksqlDbList + .clickExecuteKsqlRequestBtn(); + ksqlQueryForm + .waitUntilScreenReady() + .setQuery(SHOW_TABLES.getQuery()) + .clickExecuteBtn(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); + softly.assertAll(); + ksqlQueryForm + .clickClearResultsBtn(); + softly.assertFalse(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); + softly.assertAll(); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/schemas/SchemasTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/schemas/SchemasTest.java new file mode 100644 index 0000000000..a253bc4b67 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/schemas/SchemasTest.java @@ -0,0 +1,189 @@ +package com.provectus.kafka.ui.smokeSuite.schemas; + +import com.codeborne.selenide.Condition; +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.api.model.CompatibilityLevel; +import com.provectus.kafka.ui.models.Schema; +import io.qase.api.annotation.QaseId; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.testng.asserts.SoftAssert; + +import java.util.ArrayList; +import java.util.List; + +import static com.provectus.kafka.ui.utilities.FileUtils.fileToString; + +public class SchemasTest extends BaseTest { + + private static final List SCHEMA_LIST = new ArrayList<>(); + private static final Schema AVRO_API = Schema.createSchemaAvro(); + private static final Schema JSON_API = Schema.createSchemaJson(); + private static final Schema PROTOBUF_API = Schema.createSchemaProtobuf(); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + SCHEMA_LIST.addAll(List.of(AVRO_API, JSON_API, PROTOBUF_API)); + SCHEMA_LIST.forEach(schema -> apiService.createSchema(schema)); + } + + @QaseId(43) + @Test(priority = 1) + public void createSchemaAvro() { + Schema schemaAvro = Schema.createSchemaAvro(); + navigateToSchemaRegistry(); + schemaRegistryList + .clickCreateSchema(); + schemaCreateForm + .setSubjectName(schemaAvro.getName()) + .setSchemaField(fileToString(schemaAvro.getValuePath())) + .selectSchemaTypeFromDropdown(schemaAvro.getType()) + .clickSubmitButton(); + schemaDetails + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(schemaDetails.isSchemaHeaderVisible(schemaAvro.getName()), "isSchemaHeaderVisible()"); + softly.assertEquals(schemaDetails.getSchemaType(), schemaAvro.getType().getValue(), "getSchemaType()"); + softly.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue(), + "getCompatibility()"); + softly.assertAll(); + navigateToSchemaRegistry(); + Assert.assertTrue(schemaRegistryList.isSchemaVisible(AVRO_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.add(schemaAvro); + } + + @QaseId(186) + @Test(priority = 2) + public void updateSchemaAvro() { + AVRO_API.setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schema_avro_for_update.json"); + navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName()); + schemaDetails + .openEditSchema(); + schemaCreateForm + .waitUntilScreenReady(); + verifyElementsCondition(schemaCreateForm.getAllDetailsPageElements(), Condition.visible); + SoftAssert softly = new SoftAssert(); + softly.assertFalse(schemaCreateForm.isSubmitBtnEnabled(), "isSubmitBtnEnabled()"); + softly.assertFalse(schemaCreateForm.isSchemaDropDownEnabled(), "isSchemaDropDownEnabled()"); + softly.assertAll(); + schemaCreateForm + .selectCompatibilityLevelFromDropdown(CompatibilityLevel.CompatibilityEnum.NONE) + .setNewSchemaValue(fileToString(AVRO_API.getValuePath())) + .clickSubmitButton(); + schemaDetails + .waitUntilScreenReady(); + Assert.assertEquals(CompatibilityLevel.CompatibilityEnum.NONE.toString(), schemaDetails.getCompatibility(), "getCompatibility()"); + } + + @QaseId(44) + @Test(priority = 3) + public void compareVersionsOperation() { + navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName()); + int latestVersion = schemaDetails + .waitUntilScreenReady() + .getLatestVersion(); + schemaDetails + .openCompareVersionMenu(); + int versionsNumberFromDdl = schemaCreateForm + .waitUntilScreenReady() + .openSchemaVersionDdl() + .getVersionsNumberFromList(); + Assert.assertEquals(latestVersion, versionsNumberFromDdl, "Versions number is not matched"); + schemaCreateForm + .selectVersionFromDropDown(1); + Assert.assertEquals(53, schemaCreateForm.getMarkedLinesNumber(), "getAllMarkedLines()"); + } + + @QaseId(187) + @Test(priority = 4) + public void deleteSchemaAvro() { + navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName()); + schemaDetails + .removeSchema(); + schemaRegistryList + .waitUntilScreenReady(); + Assert.assertFalse(schemaRegistryList.isSchemaVisible(AVRO_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.remove(AVRO_API); + } + + @QaseId(89) + @Test(priority = 5) + public void createSchemaJson() { + Schema schemaJson = Schema.createSchemaJson(); + navigateToSchemaRegistry(); + schemaRegistryList + .clickCreateSchema(); + schemaCreateForm + .setSubjectName(schemaJson.getName()) + .setSchemaField(fileToString(schemaJson.getValuePath())) + .selectSchemaTypeFromDropdown(schemaJson.getType()) + .clickSubmitButton(); + schemaDetails + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(schemaDetails.isSchemaHeaderVisible(schemaJson.getName()), "isSchemaHeaderVisible()"); + softly.assertEquals(schemaDetails.getSchemaType(), schemaJson.getType().getValue(), "getSchemaType()"); + softly.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue(), + "getCompatibility()"); + softly.assertAll(); + navigateToSchemaRegistry(); + Assert.assertTrue(schemaRegistryList.isSchemaVisible(JSON_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.add(schemaJson); + } + + @QaseId(189) + @Test(priority = 6) + public void deleteSchemaJson() { + navigateToSchemaRegistryAndOpenDetails(JSON_API.getName()); + schemaDetails + .removeSchema(); + schemaRegistryList + .waitUntilScreenReady(); + Assert.assertFalse(schemaRegistryList.isSchemaVisible(JSON_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.remove(JSON_API); + } + + @QaseId(91) + @Test(priority = 7) + public void createSchemaProtobuf() { + Schema schemaProtobuf = Schema.createSchemaProtobuf(); + navigateToSchemaRegistry(); + schemaRegistryList + .clickCreateSchema(); + schemaCreateForm + .setSubjectName(schemaProtobuf.getName()) + .setSchemaField(fileToString(schemaProtobuf.getValuePath())) + .selectSchemaTypeFromDropdown(schemaProtobuf.getType()) + .clickSubmitButton(); + schemaDetails + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(schemaDetails.isSchemaHeaderVisible(schemaProtobuf.getName()), "isSchemaHeaderVisible()"); + softly.assertEquals(schemaDetails.getSchemaType(), schemaProtobuf.getType().getValue(), "getSchemaType()"); + softly.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue(), + "getCompatibility()"); + softly.assertAll(); + navigateToSchemaRegistry(); + Assert.assertTrue(schemaRegistryList.isSchemaVisible(PROTOBUF_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.add(schemaProtobuf); + } + + @QaseId(223) + @Test(priority = 8) + public void deleteSchemaProtobuf() { + navigateToSchemaRegistryAndOpenDetails(PROTOBUF_API.getName()); + schemaDetails + .removeSchema(); + schemaRegistryList + .waitUntilScreenReady(); + Assert.assertFalse(schemaRegistryList.isSchemaVisible(PROTOBUF_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.remove(PROTOBUF_API); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + SCHEMA_LIST.forEach(schema -> apiService.deleteSchema(schema.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/MessagesTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/MessagesTest.java new file mode 100644 index 0000000000..dc7fc403c8 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/MessagesTest.java @@ -0,0 +1,284 @@ +package com.provectus.kafka.ui.smokeSuite.topics; + +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.models.Topic; +import com.provectus.kafka.ui.pages.topics.TopicDetails; +import io.qameta.allure.Issue; +import io.qameta.allure.Step; +import io.qase.api.annotation.QaseId; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; +import org.testng.asserts.SoftAssert; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.IntStream; + +import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.MESSAGES; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.OVERVIEW; +import static com.provectus.kafka.ui.utilities.TimeUtils.waitUntilNewMinuteStarted; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + +public class MessagesTest extends BaseTest { + + private static final Topic TOPIC_FOR_MESSAGES = new Topic() + .setName("topic-with-clean-message-attribute-" + randomAlphabetic(5)) + .setMessageKey(randomAlphabetic(5)) + .setMessageContent(randomAlphabetic(10)); + private static final Topic TOPIC_TO_CLEAR_AND_PURGE_MESSAGES = new Topic() + .setName("topic-to-clear-and-purge-messages-attribute-" + randomAlphabetic(5)) + .setMessageKey(randomAlphabetic(5)) + .setMessageContent(randomAlphabetic(10)); + private static final Topic TOPIC_FOR_CHECK_FILTERS = new Topic() + .setName("topic-for-check-filters-" + randomAlphabetic(5)) + .setMessageKey(randomAlphabetic(5)) + .setMessageContent(randomAlphabetic(10)); + private static final Topic TOPIC_TO_RECREATE = new Topic() + .setName("topic-to-recreate-attribute-" + randomAlphabetic(5)) + .setMessageKey(randomAlphabetic(5)) + .setMessageContent(randomAlphabetic(10)); + private static final Topic TOPIC_FOR_CHECK_MESSAGES_COUNT = new Topic() + .setName("topic-for-check-messages-count" + randomAlphabetic(5)) + .setMessageKey(randomAlphabetic(5)) + .setMessageContent(randomAlphabetic(10)); + private static final List TOPIC_LIST = new ArrayList<>(); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + TOPIC_LIST.addAll(List.of(TOPIC_FOR_MESSAGES, TOPIC_FOR_CHECK_FILTERS, TOPIC_TO_CLEAR_AND_PURGE_MESSAGES, + TOPIC_TO_RECREATE, TOPIC_FOR_CHECK_MESSAGES_COUNT)); + TOPIC_LIST.forEach(topic -> apiService.createTopic(topic.getName())); + IntStream.range(1, 3).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECK_FILTERS)); + waitUntilNewMinuteStarted(); + IntStream.range(1, 3).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECK_FILTERS)); + IntStream.range(1, 110).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECK_MESSAGES_COUNT)); + } + + @QaseId(222) + @Test(priority = 1) + public void produceMessage() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_MESSAGES.getName()); + topicDetails + .openDetailsTab(MESSAGES); + produceMessage(TOPIC_FOR_MESSAGES); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicDetails.isKeyMessageVisible((TOPIC_FOR_MESSAGES.getMessageKey())), + "isKeyMessageVisible()"); + softly.assertTrue(topicDetails.isContentMessageVisible((TOPIC_FOR_MESSAGES.getMessageContent()).trim()), + "isContentMessageVisible()"); + softly.assertAll(); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/2778") + @QaseId(19) + @Test(priority = 2) + public void clearMessage() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_MESSAGES.getName()); + topicDetails + .openDetailsTab(OVERVIEW); + int messageAmount = topicDetails.getMessageCountAmount(); + produceMessage(TOPIC_FOR_MESSAGES); + Assert.assertEquals(messageAmount + 1, topicDetails.getMessageCountAmount(), "getMessageCountAmount()"); + topicDetails + .openDotMenu() + .clickClearMessagesMenu() + .waitUntilScreenReady(); + Assert.assertEquals(0, topicDetails.getMessageCountAmount(), "getMessageCountAmount()"); + } + + @QaseId(239) + @Test(priority = 3) + public void checkClearTopicMessage() { + navigateToTopicsAndOpenDetails(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()); + topicDetails + .openDetailsTab(OVERVIEW); + produceMessage(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES); + navigateToTopics(); + Assert.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), 1, + "getNumberOfMessages()"); + topicsList + .openDotMenuByTopicName(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()) + .clickClearMessagesBtn() + .clickConfirmBtnMdl(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS, + String.format("%s messages have been successfully cleared!", TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName())), + "isAlertWithMessageVisible()"); + softly.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), 0, + "getNumberOfMessages()"); + softly.assertAll(); + } + + @QaseId(10) + @Test(priority = 4) + public void checkPurgeMessagePossibility() { + navigateToTopics(); + int messageAmount = topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(); + topicsList + .openTopic(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()); + topicDetails + .openDetailsTab(OVERVIEW); + produceMessage(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES); + navigateToTopics(); + Assert.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), + messageAmount + 1, "getNumberOfMessages()"); + topicsList + .getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()) + .selectItem(true) + .clickPurgeMessagesOfSelectedTopicsBtn(); + Assert.assertTrue(topicsList.isConfirmationMdlVisible(), "isConfirmationMdlVisible()"); + topicsList + .clickCancelBtnMdl() + .clickPurgeMessagesOfSelectedTopicsBtn() + .clickConfirmBtnMdl(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS, + String.format("%s messages have been successfully cleared!", TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName())), + "isAlertWithMessageVisible()"); + softly.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), 0, + "getNumberOfMessages()"); + softly.assertAll(); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/2394") + @QaseId(15) + @Test(priority = 6) + public void checkMessageFilteringByOffset() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(MESSAGES); + TopicDetails.MessageGridItem secondMessage = topicDetails.getMessageByOffset(1); + topicDetails + .selectSeekTypeDdlMessagesTab("Offset") + .setSeekTypeValueFldMessagesTab(String.valueOf(secondMessage.getOffset())) + .clickSubmitFiltersBtnMessagesTab(); + SoftAssert softly = new SoftAssert(); + topicDetails.getAllMessages().forEach(message -> + softly.assertTrue(message.getOffset() == secondMessage.getOffset() + || message.getOffset() > secondMessage.getOffset(), + String.format("Expected offset is: %s, but found: %s", secondMessage.getOffset(), message.getOffset()))); + softly.assertAll(); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/3215") + @Issue("https://github.com/provectus/kafka-ui/issues/2345") + @QaseId(16) + @Test(priority = 7) + public void checkMessageFilteringByTimestamp() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(MESSAGES); + LocalDateTime firstTimestamp = topicDetails.getMessageByOffset(0).getTimestamp(); + List nextMessages = topicDetails.getAllMessages().stream() + .filter(message -> message.getTimestamp().getMinute() != firstTimestamp.getMinute()) + .toList(); + LocalDateTime nextTimestamp = Objects.requireNonNull(nextMessages.stream() + .findFirst().orElseThrow()).getTimestamp(); + topicDetails + .selectSeekTypeDdlMessagesTab("Timestamp") + .openCalendarSeekType() + .selectDateAndTimeByCalendar(nextTimestamp) + .clickSubmitFiltersBtnMessagesTab(); + SoftAssert softly = new SoftAssert(); + topicDetails.getAllMessages().forEach(message -> + softly.assertTrue(message.getTimestamp().isEqual(nextTimestamp) + || message.getTimestamp().isAfter(nextTimestamp), + String.format("Expected timestamp is: %s, but found: %s", nextTimestamp, message.getTimestamp()))); + softly.assertAll(); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/2778") + @QaseId(246) + @Test(priority = 8) + public void checkClearTopicMessageFromOverviewTab() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(OVERVIEW) + .openDotMenu() + .clickClearMessagesMenu() + .clickConfirmBtnMdl(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, + String.format("%s messages have been successfully cleared!", TOPIC_FOR_CHECK_FILTERS.getName())), + "isAlertWithMessageVisible()"); + softly.assertEquals(topicDetails.getMessageCountAmount(), 0, + "getMessageCountAmount()= " + topicDetails.getMessageCountAmount()); + softly.assertAll(); + } + + @QaseId(240) + @Test(priority = 9) + public void checkRecreateTopic() { + navigateToTopicsAndOpenDetails(TOPIC_TO_RECREATE.getName()); + topicDetails + .openDetailsTab(OVERVIEW); + produceMessage(TOPIC_TO_RECREATE); + navigateToTopics(); + Assert.assertEquals(topicsList.getTopicItem(TOPIC_TO_RECREATE.getName()).getNumberOfMessages(), 1, + "getNumberOfMessages()"); + topicsList + .openDotMenuByTopicName(TOPIC_TO_RECREATE.getName()) + .clickRecreateTopicBtn() + .clickConfirmBtnMdl(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, + String.format("Topic %s successfully recreated!", TOPIC_TO_RECREATE.getName())), + "isAlertWithMessageVisible()"); + softly.assertEquals(topicsList.getTopicItem(TOPIC_TO_RECREATE.getName()).getNumberOfMessages(), 0, + "getNumberOfMessages()"); + softly.assertAll(); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/3129") + @QaseId(267) + @Test(priority = 10) + public void CheckMessagesCountPerPageWithinTopic() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_MESSAGES_COUNT.getName()); + topicDetails + .openDetailsTab(MESSAGES); + int messagesPerPage = topicDetails.getAllMessages().size(); + SoftAssert softly = new SoftAssert(); + softly.assertEquals(messagesPerPage, 100, "getAllMessages()"); + softly.assertFalse(topicDetails.isBackButtonEnabled(), "isBackButtonEnabled()"); + softly.assertTrue(topicDetails.isNextButtonEnabled(), "isNextButtonEnabled()"); + softly.assertAll(); + int lastOffsetOnPage = topicDetails.getAllMessages() + .get(messagesPerPage - 1).getOffset(); + topicDetails + .clickNextButton(); + softly.assertEquals(topicDetails.getAllMessages().stream().findFirst().orElseThrow().getOffset(), + lastOffsetOnPage + 1, "findFirst().getOffset()"); + softly.assertTrue(topicDetails.isBackButtonEnabled(), "isBackButtonEnabled()"); + softly.assertFalse(topicDetails.isNextButtonEnabled(), "isNextButtonEnabled()"); + softly.assertAll(); + } + + @Step + protected void produceMessage(Topic topic) { + topicDetails + .clickProduceMessageBtn(); + produceMessagePanel + .waitUntilScreenReady() + .setKeyField(topic.getMessageKey()) + .setContentFiled(topic.getMessageContent()) + .submitProduceMessage(); + topicDetails + .waitUntilScreenReady(); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/TopicsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/TopicsTest.java new file mode 100644 index 0000000000..3f1347cb39 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/TopicsTest.java @@ -0,0 +1,483 @@ +package com.provectus.kafka.ui.smokeSuite.topics; + +import com.codeborne.selenide.Condition; +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.models.Topic; +import com.provectus.kafka.ui.pages.topics.TopicDetails; +import io.qameta.allure.Issue; +import io.qase.api.annotation.QaseId; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; +import org.testng.asserts.SoftAssert; + +import java.util.ArrayList; +import java.util.List; + +import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.MESSAGES; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.SETTINGS; +import static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.COMPACT; +import static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.DELETE; +import static com.provectus.kafka.ui.pages.topics.enums.CustomParameterType.COMPRESSION_TYPE; +import static com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk.*; +import static com.provectus.kafka.ui.pages.topics.enums.TimeToRetain.BTN_2_DAYS; +import static com.provectus.kafka.ui.pages.topics.enums.TimeToRetain.BTN_7_DAYS; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; +import static org.apache.commons.lang3.RandomUtils.nextInt; + +public class TopicsTest extends BaseTest { + + private static final Topic TOPIC_TO_CREATE = new Topic() + .setName("new-topic-" + randomAlphabetic(5)) + .setNumberOfPartitions(1) + .setCustomParameterType(COMPRESSION_TYPE) + .setCustomParameterValue("producer") + .setCleanupPolicyValue(DELETE); + private static final Topic TOPIC_TO_UPDATE_AND_DELETE = new Topic() + .setName("topic-to-update-and-delete-" + randomAlphabetic(5)) + .setNumberOfPartitions(1) + .setCleanupPolicyValue(DELETE) + .setTimeToRetain(BTN_7_DAYS) + .setMaxSizeOnDisk(NOT_SET) + .setMaxMessageBytes("1048588") + .setMessageKey(randomAlphabetic(5)) + .setMessageContent(randomAlphabetic(10)); + private static final Topic TOPIC_TO_CHECK_SETTINGS = new Topic() + .setName("new-topic-" + randomAlphabetic(5)) + .setNumberOfPartitions(1) + .setMaxMessageBytes("1000012") + .setMaxSizeOnDisk(NOT_SET); + private static final Topic TOPIC_FOR_CHECK_FILTERS = new Topic() + .setName("topic-for-check-filters-" + randomAlphabetic(5)); + private static final Topic TOPIC_FOR_DELETE = new Topic().setName("topic-to-delete-" + randomAlphabetic(5)); + private static final List TOPIC_LIST = new ArrayList<>(); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + TOPIC_LIST.addAll(List.of(TOPIC_TO_UPDATE_AND_DELETE, TOPIC_FOR_DELETE, TOPIC_FOR_CHECK_FILTERS)); + TOPIC_LIST.forEach(topic -> apiService.createTopic(topic.getName())); + } + + @QaseId(199) + @Test(priority = 1) + public void createTopic() { + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(TOPIC_TO_CREATE.getName()) + .setNumberOfPartitions(TOPIC_TO_CREATE.getNumberOfPartitions()) + .selectCleanupPolicy(TOPIC_TO_CREATE.getCleanupPolicyValue()) + .clickCreateTopicBtn(); + navigateToTopicsAndOpenDetails(TOPIC_TO_CREATE.getName()); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicDetails.isTopicHeaderVisible(TOPIC_TO_CREATE.getName()), "isTopicHeaderVisible()"); + softly.assertEquals(topicDetails.getCleanUpPolicy(), TOPIC_TO_CREATE.getCleanupPolicyValue().toString(), "getCleanUpPolicy()"); + softly.assertEquals(topicDetails.getPartitions(), TOPIC_TO_CREATE.getNumberOfPartitions(), "getPartitions()"); + softly.assertAll(); + navigateToTopics(); + Assert.assertTrue(topicsList.isTopicVisible(TOPIC_TO_CREATE.getName()), "isTopicVisible()"); + TOPIC_LIST.add(TOPIC_TO_CREATE); + } + + @QaseId(7) + @Test(priority = 2) + void checkAvailableOperations() { + navigateToTopics(); + topicsList + .getTopicItem("my_ksql_1ksql_processing_log") + .selectItem(true); + verifyElementsCondition(topicsList.getActionButtons(), Condition.enabled); + topicsList + .getTopicItem("_confluent-ksql-my_ksql_1_command_topic") + .selectItem(true); + Assert.assertFalse(topicsList.isCopySelectedTopicBtnEnabled(), "isCopySelectedTopicBtnEnabled()"); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/3071") + @QaseId(268) + @Test(priority = 3) + public void checkCustomParametersWithinEditExistingTopic() { + navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName()); + topicDetails + .openDotMenu() + .clickEditSettingsMenu(); + SoftAssert softly = new SoftAssert(); + topicCreateEditForm + .waitUntilScreenReady() + .clickAddCustomParameterTypeButton() + .openCustomParameterTypeDdl() + .getAllDdlOptions() + .forEach(option -> + softly.assertTrue(!option.is(Condition.attribute("disabled")), + option.getText() + " is enabled:")); + softly.assertAll(); + } + + @QaseId(197) + @Test(priority = 4) + public void updateTopic() { + navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName()); + topicDetails + .openDotMenu() + .clickEditSettingsMenu(); + topicCreateEditForm + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertEquals(topicCreateEditForm.getCleanupPolicy(), + TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue().getVisibleText(), "getCleanupPolicy()"); + softly.assertEquals(topicCreateEditForm.getTimeToRetain(), + TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetain().getValue(), "getTimeToRetain()"); + softly.assertEquals(topicCreateEditForm.getMaxSizeOnDisk(), + TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk().getVisibleText(), "getMaxSizeOnDisk()"); + softly.assertEquals(topicCreateEditForm.getMaxMessageBytes(), + TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes(), "getMaxMessageBytes()"); + softly.assertAll(); + TOPIC_TO_UPDATE_AND_DELETE + .setCleanupPolicyValue(COMPACT) + .setTimeToRetain(BTN_2_DAYS) + .setMaxSizeOnDisk(SIZE_50_GB).setMaxMessageBytes("1048589"); + topicCreateEditForm + .selectCleanupPolicy((TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue())) + .setTimeToRetainDataByButtons(TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetain()) + .setMaxSizeOnDiskInGB(TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk()) + .setMaxMessageBytes(TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes()) + .clickCreateTopicBtn(); + softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, "Topic successfully updated."), + "isAlertWithMessageVisible()"); + softly.assertTrue(topicDetails.isTopicHeaderVisible(TOPIC_TO_UPDATE_AND_DELETE.getName()), + "isTopicHeaderVisible()"); + softly.assertAll(); + topicDetails + .waitUntilScreenReady(); + navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName()); + topicDetails + .openDotMenu() + .clickEditSettingsMenu(); + softly.assertFalse(topicCreateEditForm.isNameFieldEnabled(), "isNameFieldEnabled()"); + softly.assertEquals(topicCreateEditForm.getCleanupPolicy(), + TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue().getVisibleText(), "getCleanupPolicy()"); + softly.assertEquals(topicCreateEditForm.getTimeToRetain(), + TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetain().getValue(), "getTimeToRetain()"); + softly.assertEquals(topicCreateEditForm.getMaxSizeOnDisk(), + TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk().getVisibleText(), "getMaxSizeOnDisk()"); + softly.assertEquals(topicCreateEditForm.getMaxMessageBytes(), + TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes(), "getMaxMessageBytes()"); + softly.assertAll(); + } + + @QaseId(242) + @Test(priority = 5) + public void removeTopicFromTopicList() { + navigateToTopics(); + topicsList + .openDotMenuByTopicName(TOPIC_TO_UPDATE_AND_DELETE.getName()) + .clickRemoveTopicBtn() + .clickConfirmBtnMdl(); + Assert.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS, + String.format("Topic %s successfully deleted!", TOPIC_TO_UPDATE_AND_DELETE.getName())), + "isAlertWithMessageVisible()"); + TOPIC_LIST.remove(TOPIC_TO_UPDATE_AND_DELETE); + } + + @QaseId(207) + @Test(priority = 6) + public void deleteTopic() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_DELETE.getName()); + topicDetails + .openDotMenu() + .clickDeleteTopicMenu() + .clickConfirmBtnMdl(); + navigateToTopics(); + Assert.assertFalse(topicsList.isTopicVisible(TOPIC_FOR_DELETE.getName()), "isTopicVisible"); + TOPIC_LIST.remove(TOPIC_FOR_DELETE); + } + + @QaseId(20) + @Test(priority = 7) + public void redirectToConsumerFromTopic() { + String topicName = "source-activities"; + String consumerGroupId = "connect-sink_postgres_activities"; + navigateToTopicsAndOpenDetails(topicName); + topicDetails + .openDetailsTab(TopicDetails.TopicMenu.CONSUMERS) + .openConsumerGroup(consumerGroupId); + consumersDetails + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(consumersDetails.isRedirectedConsumerTitleVisible(consumerGroupId), + "isRedirectedConsumerTitleVisible()"); + softly.assertTrue(consumersDetails.isTopicInConsumersDetailsVisible(topicName), + "isTopicInConsumersDetailsVisible()"); + softly.assertAll(); + } + + @QaseId(4) + @Test(priority = 8) + public void checkTopicCreatePossibility() { + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady(); + Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); + topicCreateEditForm + .setTopicName("testName"); + Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); + topicCreateEditForm + .setTopicName(null) + .setNumberOfPartitions(nextInt(1, 10)); + Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); + topicCreateEditForm + .setTopicName("testName"); + Assert.assertTrue(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); + } + + @QaseId(266) + @Test(priority = 9) + public void checkTimeToRetainDataCustomValueWithEditingTopic() { + Topic topicToRetainData = new Topic() + .setName("topic-to-retain-data-" + randomAlphabetic(5)) + .setTimeToRetainData("86400000"); + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(topicToRetainData.getName()) + .setNumberOfPartitions(1) + .setTimeToRetainDataInMs("604800000"); + Assert.assertEquals(topicCreateEditForm.getTimeToRetain(), "604800000", "getTimeToRetain()"); + topicCreateEditForm + .setTimeToRetainDataInMs(topicToRetainData.getTimeToRetainData()) + .clickCreateTopicBtn(); + topicDetails + .waitUntilScreenReady() + .openDotMenu() + .clickEditSettingsMenu(); + Assert.assertEquals(topicCreateEditForm.getTimeToRetain(), topicToRetainData.getTimeToRetainData(), + "getTimeToRetain()"); + topicDetails + .openDetailsTab(SETTINGS); + Assert.assertEquals(topicDetails.getSettingsGridValueByKey("retention.ms"), topicToRetainData.getTimeToRetainData(), + "getSettingsGridValueByKey()"); + TOPIC_LIST.add(topicToRetainData); + } + + @QaseId(6) + @Test(priority = 10) + public void checkCustomParametersWithinCreateNewTopic() { + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(TOPIC_TO_CREATE.getName()) + .clickAddCustomParameterTypeButton() + .setCustomParameterType(TOPIC_TO_CREATE.getCustomParameterType()); + Assert.assertTrue(topicCreateEditForm.isDeleteCustomParameterButtonEnabled(), + "isDeleteCustomParameterButtonEnabled()"); + topicCreateEditForm + .clearCustomParameterValue(); + Assert.assertTrue(topicCreateEditForm.isValidationMessageCustomParameterValueVisible(), + "isValidationMessageCustomParameterValueVisible()"); + } + + @QaseId(2) + @Test(priority = 11) + public void checkTopicListElements() { + navigateToTopics(); + verifyElementsCondition(topicsList.getAllVisibleElements(), Condition.visible); + verifyElementsCondition(topicsList.getAllEnabledElements(), Condition.enabled); + } + + @QaseId(12) + @Test(priority = 12) + public void addNewFilterWithinTopic() { + String filterName = randomAlphabetic(5); + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(MESSAGES) + .clickMessagesAddFiltersBtn() + .waitUntilAddFiltersMdlVisible(); + verifyElementsCondition(topicDetails.getAllAddFilterModalVisibleElements(), Condition.visible); + verifyElementsCondition(topicDetails.getAllAddFilterModalEnabledElements(), Condition.enabled); + verifyElementsCondition(topicDetails.getAllAddFilterModalDisabledElements(), Condition.disabled); + Assert.assertFalse(topicDetails.isSaveThisFilterCheckBoxSelected(), "isSaveThisFilterCheckBoxSelected()"); + topicDetails + .setFilterCodeFieldAddFilterMdl(filterName); + Assert.assertTrue(topicDetails.isAddFilterBtnAddFilterMdlEnabled(), "isAddFilterBtnAddFilterMdlEnabled()"); + topicDetails.clickAddFilterBtnAndCloseMdl(true); + Assert.assertTrue(topicDetails.isActiveFilterVisible(filterName), "isActiveFilterVisible()"); + } + + @QaseId(13) + @Test(priority = 13) + public void checkFilterSavingWithinSavedFilters() { + String displayName = randomAlphabetic(5); + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(MESSAGES) + .clickMessagesAddFiltersBtn() + .waitUntilAddFiltersMdlVisible() + .setFilterCodeFieldAddFilterMdl(randomAlphabetic(4)) + .selectSaveThisFilterCheckboxMdl(true) + .setDisplayNameFldAddFilterMdl(displayName); + Assert.assertTrue(topicDetails.isAddFilterBtnAddFilterMdlEnabled(), + "isAddFilterBtnAddFilterMdlEnabled()"); + topicDetails + .clickAddFilterBtnAndCloseMdl(false) + .openSavedFiltersListMdl(); + Assert.assertTrue(topicDetails.isFilterVisibleAtSavedFiltersMdl(displayName), + "isFilterVisibleAtSavedFiltersMdl()"); + } + + @QaseId(14) + @Test(priority = 14) + public void checkApplyingSavedFilterWithinTopicMessages() { + String displayName = randomAlphabetic(5); + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(MESSAGES) + .clickMessagesAddFiltersBtn() + .waitUntilAddFiltersMdlVisible() + .setFilterCodeFieldAddFilterMdl(randomAlphabetic(4)) + .selectSaveThisFilterCheckboxMdl(true) + .setDisplayNameFldAddFilterMdl(displayName) + .clickAddFilterBtnAndCloseMdl(false) + .openSavedFiltersListMdl() + .selectFilterAtSavedFiltersMdl(displayName) + .clickSelectFilterBtnAtSavedFiltersMdl(); + Assert.assertTrue(topicDetails.isActiveFilterVisible(displayName), "isActiveFilterVisible()"); + } + + @QaseId(11) + @Test(priority = 15) + public void checkShowInternalTopicsButtonFunctionality() { + navigateToTopics(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicsList.isShowInternalRadioBtnSelected(), "isInternalRadioBtnSelected()"); + softly.assertTrue(topicsList.getInternalTopics().size() > 0, "getInternalTopics()"); + softly.assertTrue(topicsList.getNonInternalTopics().size() > 0, "getNonInternalTopics()"); + softly.assertAll(); + topicsList + .setShowInternalRadioButton(false); + softly.assertEquals(topicsList.getInternalTopics().size(), 0, "getInternalTopics()"); + softly.assertTrue(topicsList.getNonInternalTopics().size() > 0, "getNonInternalTopics()"); + softly.assertAll(); + } + + @QaseId(56) + @Test(priority = 16) + public void checkRetentionBytesAccordingToMaxSizeOnDisk() { + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(TOPIC_TO_CHECK_SETTINGS.getName()) + .setNumberOfPartitions(TOPIC_TO_CHECK_SETTINGS.getNumberOfPartitions()) + .setMaxMessageBytes(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes()) + .clickCreateTopicBtn(); + topicDetails + .waitUntilScreenReady(); + TOPIC_LIST.add(TOPIC_TO_CHECK_SETTINGS); + topicDetails + .openDetailsTab(SETTINGS); + topicSettingsTab + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertEquals(topicSettingsTab.getValueByKey("retention.bytes"), + TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk().getOptionValue(), "getValueOfKey(retention.bytes)"); + softly.assertEquals(topicSettingsTab.getValueByKey("max.message.bytes"), + TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes(), "getValueOfKey(max.message.bytes)"); + softly.assertAll(); + TOPIC_TO_CHECK_SETTINGS + .setMaxSizeOnDisk(SIZE_1_GB) + .setMaxMessageBytes("1000056"); + topicDetails + .openDotMenu() + .clickEditSettingsMenu(); + topicCreateEditForm + .waitUntilScreenReady() + .setMaxSizeOnDiskInGB(TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk()) + .setMaxMessageBytes(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes()) + .clickCreateTopicBtn(); + topicDetails + .waitUntilScreenReady() + .openDetailsTab(SETTINGS); + topicSettingsTab + .waitUntilScreenReady(); + softly.assertEquals(topicSettingsTab.getValueByKey("retention.bytes"), + TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk().getOptionValue(), "getValueOfKey(retention.bytes)"); + softly.assertEquals(topicSettingsTab.getValueByKey("max.message.bytes"), + TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes(), "getValueOfKey(max.message.bytes)"); + softly.assertAll(); + } + + @QaseId(247) + @Test(priority = 17) + public void recreateTopicFromTopicProfile() { + Topic topicToRecreate = new Topic() + .setName("topic-to-recreate-" + randomAlphabetic(5)) + .setNumberOfPartitions(1); + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(topicToRecreate.getName()) + .setNumberOfPartitions(topicToRecreate.getNumberOfPartitions()) + .clickCreateTopicBtn(); + topicDetails + .waitUntilScreenReady(); + TOPIC_LIST.add(topicToRecreate); + topicDetails + .openDotMenu() + .clickRecreateTopicMenu(); + Assert.assertTrue(topicDetails.isConfirmationMdlVisible(), "isConfirmationMdlVisible()"); + topicDetails + .clickConfirmBtnMdl(); + Assert.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, + String.format("Topic %s successfully recreated!", topicToRecreate.getName())), + "isAlertWithMessageVisible()"); + } + + @QaseId(8) + @Test(priority = 18) + public void checkCopyTopicPossibility() { + Topic topicToCopy = new Topic() + .setName("topic-to-copy-" + randomAlphabetic(5)) + .setNumberOfPartitions(1); + navigateToTopics(); + topicsList + .getTopicItem("_schemas") + .selectItem(true) + .clickCopySelectedTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady(); + Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); + topicCreateEditForm + .setTopicName(topicToCopy.getName()) + .setNumberOfPartitions(topicToCopy.getNumberOfPartitions()) + .clickCreateTopicBtn(); + topicDetails + .waitUntilScreenReady(); + TOPIC_LIST.add(topicToCopy); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, "Topic successfully created."), + "isAlertWithMessageVisible()"); + softly.assertTrue(topicDetails.isTopicHeaderVisible(topicToCopy.getName()), "isTopicHeaderVisible()"); + softly.assertAll(); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/SmokeTests.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/SmokeTests.java deleted file mode 100644 index ac5f5c5755..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/SmokeTests.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.provectus.kafka.ui.suite; - -import com.codeborne.selenide.Condition; -import com.provectus.kafka.ui.base.BaseTest; -import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.AutomationStatus; -import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status; -import io.qase.api.annotation.CaseId; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.junit.jupiter.api.Test; - -public class SmokeTests extends BaseTest { - - @Test - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(198) - public void checkBasePageElements(){ - verifyElementsCondition(Stream.concat(topPanel.getAllVisibleElements().stream(), naviSideBar.getAllMenuButtons().stream()) - .collect(Collectors.toList()),Condition.visible); - verifyElementsCondition(Stream.concat(topPanel.getAllEnabledElements().stream(), naviSideBar.getAllMenuButtons().stream()) - .collect(Collectors.toList()),Condition.enabled); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/brokers/BrokersTests.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/brokers/BrokersTests.java deleted file mode 100644 index 0cba13812c..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/brokers/BrokersTests.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.provectus.kafka.ui.suite.brokers; - -import static com.provectus.kafka.ui.pages.NaviSideBar.SideMenuOption.BROKERS; -import static com.provectus.kafka.ui.pages.brokers.BrokersDetails.DetailsTab.CONFIGS; -import static org.assertj.core.api.Assertions.assertThat; - -import com.codeborne.selenide.Condition; -import com.provectus.kafka.ui.base.BaseTest; -import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.AutomationStatus; -import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.Suite; -import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status; -import io.qameta.allure.Step; -import io.qase.api.annotation.CaseId; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -public class BrokersTests extends BaseTest { - private static final String SUITE_TITLE = "Brokers"; - private static final long SUITE_ID = 1; - - @DisplayName("Checking the Brokers overview") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(1) - @Test - public void checkBrokersOverview(){ - navigateToBrokers(); - assertThat(brokersList.getAllBrokers()).as("getAllBrokers()").size().isGreaterThan(0); - verifyElementsCondition(brokersList.getAllVisibleElements(), Condition.visible); - verifyElementsCondition(brokersList.getAllEnabledElements(), Condition.enabled); - } - - @DisplayName("Checking the existing Broker's profile in a cluster") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(85) - @Test - public void checkExistingBrokersInCluster(){ - navigateToBrokers(); - assertThat(brokersList.getAllBrokers()).as("getAllBrokers()").size().isGreaterThan(0); - brokersList - .openBroker(1); - brokersDetails - .waitUntilScreenReady(); - verifyElementsCondition(brokersDetails.getAllVisibleElements(), Condition.visible); - verifyElementsCondition(brokersDetails.getAllEnabledElements(), Condition.enabled); - brokersDetails - .openDetailsTab(CONFIGS); - brokersConfigTab - .waitUntilScreenReady(); - verifyElementsCondition(brokersConfigTab.getColumnHeaders(), Condition.visible); - verifyElementsCondition(brokersConfigTab.getEditButtons(), Condition.enabled); - assertThat(brokersConfigTab.isSearchByKeyVisible()).as("isSearchByKeyVisible()").isTrue(); - } - - @Step - private void navigateToBrokers(){ - naviSideBar - .openSideMenu(BROKERS); - brokersList - .waitUntilScreenReady(); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/connectors/ConnectorsTests.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/connectors/ConnectorsTests.java deleted file mode 100644 index ec2e3d5a0f..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/connectors/ConnectorsTests.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.provectus.kafka.ui.suite.connectors; - -import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; -import static com.provectus.kafka.ui.pages.NaviSideBar.SideMenuOption.KAFKA_CONNECT; -import static com.provectus.kafka.ui.utilities.FileUtils.getResourceAsString; -import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; - -import com.provectus.kafka.ui.base.BaseTest; -import com.provectus.kafka.ui.models.Connector; -import com.provectus.kafka.ui.models.Topic; -import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.AutomationStatus; -import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.Suite; -import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status; -import io.qameta.allure.Step; -import io.qase.api.annotation.CaseId; -import java.util.ArrayList; -import java.util.List; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class ConnectorsTests extends BaseTest { - private static final long SUITE_ID = 10; - private static final String SUITE_TITLE = "Kafka Connect"; - private static final String CONNECT_NAME = "first"; - private static final List TOPIC_LIST = new ArrayList<>(); - private static final List CONNECTOR_LIST = new ArrayList<>(); - private static final String MESSAGE_CONTENT = "message_content_create_topic.json"; - private static final String MESSAGE_KEY = " "; - private static final Topic TOPIC_FOR_CREATE = new Topic() - .setName("topic_for_create_connector-" + randomAlphabetic(5)) - .setMessageContent(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); - private static final Topic TOPIC_FOR_DELETE = new Topic() - .setName("topic_for_delete_connector-" + randomAlphabetic(5)) - .setMessageContent(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); - private static final Topic TOPIC_FOR_UPDATE = new Topic() - .setName("topic_for_update_connector-" + randomAlphabetic(5)) - .setMessageContent(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); - private static final Connector CONNECTOR_FOR_DELETE = new Connector() - .setName("sink_postgres_activities_e2e_checks_for_delete-" + randomAlphabetic(5)) - .setConfig(getResourceAsString("delete_connector_config.json")); - private static final Connector CONNECTOR_FOR_UPDATE = new Connector() - .setName("sink_postgres_activities_e2e_checks_for_update-" + randomAlphabetic(5)) - .setConfig(getResourceAsString("config_for_create_connector_via_api.json")); - - @BeforeAll - public void beforeAll() { - TOPIC_LIST.addAll(List.of(TOPIC_FOR_CREATE, TOPIC_FOR_DELETE, TOPIC_FOR_UPDATE)); - TOPIC_LIST.forEach(topic -> apiService - .createTopic(topic.getName()) - .sendMessage(topic) - ); - CONNECTOR_LIST.addAll(List.of(CONNECTOR_FOR_DELETE, CONNECTOR_FOR_UPDATE)); - CONNECTOR_LIST.forEach(connector -> apiService - .createConnector(CONNECT_NAME, connector)); - } - - @DisplayName("should create a connector") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(42) - @Test - public void createConnector() { - Connector connectorForCreate = new Connector() - .setName("sink_postgres_activities_e2e_checks-" + randomAlphabetic(5)) - .setConfig(getResourceAsString("config_for_create_connector.json")); - navigateToConnectors(); - kafkaConnectList - .clickCreateConnectorBtn(); - connectorCreateForm - .waitUntilScreenReady() - .setConnectorDetails(connectorForCreate.getName(), connectorForCreate.getConfig()) - .clickSubmitButton(); - connectorDetails - .waitUntilScreenReady(); - navigateToConnectorsAndOpenDetails(connectorForCreate.getName()); - Assertions.assertTrue(connectorDetails.isConnectorHeaderVisible(connectorForCreate.getName()),"isConnectorTitleVisible()"); - navigateToConnectors(); - Assertions.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), "isConnectorVisible()"); - CONNECTOR_LIST.add(connectorForCreate); - } - - @DisplayName("should update a connector") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(196) - @Test - public void updateConnector() { - navigateToConnectorsAndOpenDetails(CONNECTOR_FOR_UPDATE.getName()); - connectorDetails - .openConfigTab() - .setConfig(CONNECTOR_FOR_UPDATE.getConfig()) - .clickSubmitButton(); - Assertions.assertTrue(connectorDetails.isAlertWithMessageVisible(SUCCESS,"Config successfully updated."),"isAlertWithMessageVisible()"); - navigateToConnectors(); - Assertions.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_UPDATE.getName()), "isConnectorVisible()"); - } - - @DisplayName("should delete connector") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(195) - @Test - public void deleteConnector() { - navigateToConnectorsAndOpenDetails(CONNECTOR_FOR_DELETE.getName()); - connectorDetails - .openDotMenu() - .clickDeleteBtn() - .clickConfirmBtn(); - navigateToConnectors(); - Assertions.assertFalse(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), "isConnectorVisible()"); - CONNECTOR_LIST.remove(CONNECTOR_FOR_DELETE); - } - - @AfterAll - public void afterAll() { - CONNECTOR_LIST.forEach(connector -> - apiService.deleteConnector(CONNECT_NAME, connector.getName())); - TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); - } - - @Step - private void navigateToConnectors(){ - naviSideBar - .openSideMenu(KAFKA_CONNECT); - kafkaConnectList - .waitUntilScreenReady(); - } - - @Step - private void navigateToConnectorsAndOpenDetails(String connectorName){ - navigateToConnectors(); - kafkaConnectList - .openConnector(connectorName); - connectorDetails - .waitUntilScreenReady(); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/schemas/SchemasTests.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/schemas/SchemasTests.java deleted file mode 100644 index ea98b4abe1..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/schemas/SchemasTests.java +++ /dev/null @@ -1,249 +0,0 @@ -package com.provectus.kafka.ui.suite.schemas; - -import static com.provectus.kafka.ui.pages.NaviSideBar.SideMenuOption.SCHEMA_REGISTRY; -import static com.provectus.kafka.ui.utilities.FileUtils.fileToString; - -import com.codeborne.selenide.Condition; -import com.provectus.kafka.ui.api.model.CompatibilityLevel; -import com.provectus.kafka.ui.base.BaseTest; -import com.provectus.kafka.ui.models.Schema; -import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.AutomationStatus; -import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.Suite; -import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status; -import io.qameta.allure.Step; -import io.qase.api.annotation.CaseId; -import java.util.ArrayList; -import java.util.List; -import lombok.SneakyThrows; -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.TestMethodOrder; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class SchemasTests extends BaseTest { - private static final long SUITE_ID = 11; - private static final String SUITE_TITLE = "Schema Registry"; - private static final List SCHEMA_LIST = new ArrayList<>(); - private static final Schema AVRO_API = Schema.createSchemaAvro(); - private static final Schema JSON_API = Schema.createSchemaJson(); - private static final Schema PROTOBUF_API = Schema.createSchemaProtobuf(); - - @BeforeAll - @SneakyThrows - public void beforeAll() { - SCHEMA_LIST.addAll(List.of(AVRO_API, JSON_API, PROTOBUF_API)); - SCHEMA_LIST.forEach(schema -> apiService.createSchema(schema)); - } - - @DisplayName("should create AVRO schema") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(43) - @Test - @Order(1) - void createSchemaAvro() { - Schema schemaAvro = Schema.createSchemaAvro(); - navigateToSchemaRegistry(); - schemaRegistryList - .clickCreateSchema(); - schemaCreateForm - .setSubjectName(schemaAvro.getName()) - .setSchemaField(fileToString(schemaAvro.getValuePath())) - .selectSchemaTypeFromDropdown(schemaAvro.getType()) - .clickSubmitButton(); - schemaDetails - .waitUntilScreenReady(); - SoftAssertions softly = new SoftAssertions(); - softly.assertThat(schemaDetails.isSchemaHeaderVisible(schemaAvro.getName())).as("isSchemaHeaderVisible()").isTrue(); - softly.assertThat(schemaDetails.getSchemaType()).as("getSchemaType()").isEqualTo(schemaAvro.getType().getValue()); - softly.assertThat(schemaDetails.getCompatibility()).as("getCompatibility()").isEqualTo(CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue()); - softly.assertAll(); - navigateToSchemaRegistry(); - Assertions.assertTrue(schemaRegistryList.isSchemaVisible(AVRO_API.getName()),"isSchemaVisible()"); - SCHEMA_LIST.add(schemaAvro); - } - - @DisplayName("should update AVRO schema") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(186) - @Test - @Order(2) - void updateSchemaAvro() { - AVRO_API.setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schema_avro_for_update.json"); - navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName()); - schemaDetails - .openEditSchema(); - schemaCreateForm - .waitUntilScreenReady(); - verifyElementsCondition(schemaCreateForm.getAllDetailsPageElements(), Condition.visible); - SoftAssertions softly = new SoftAssertions(); - softly.assertThat(schemaCreateForm.isSubmitBtnEnabled()).as("isSubmitBtnEnabled()").isFalse(); - softly.assertThat(schemaCreateForm.isSchemaDropDownEnabled()).as("isSchemaDropDownEnabled()").isFalse(); - softly.assertAll(); - schemaCreateForm - .selectCompatibilityLevelFromDropdown(CompatibilityLevel.CompatibilityEnum.NONE) - .setNewSchemaValue(fileToString(AVRO_API.getValuePath())) - .clickSubmitButton(); - schemaDetails - .waitUntilScreenReady(); - Assertions.assertEquals(CompatibilityLevel.CompatibilityEnum.NONE.toString(), schemaDetails.getCompatibility(), "getCompatibility()"); - } - - @DisplayName("Checking Compare Versions operation for Schema") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(186) - @Test - @Order(3) - void compareVersionsOperation() { - navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName()); - int latestVersion = schemaDetails - .waitUntilScreenReady() - .getLatestVersion(); - schemaDetails - .openCompareVersionMenu(); - int versionsNumberFromDdl = schemaCreateForm - .waitUntilScreenReady() - .openSchemaVersionDdl() - .getVersionsNumberFromList(); - Assertions.assertEquals(latestVersion,versionsNumberFromDdl,"Versions number is not matched"); - schemaCreateForm - .selectVersionFromDropDown(1); - Assertions.assertEquals(53, schemaCreateForm.getMarkedLinesNumber(), "getAllMarkedLines()"); - } - - @DisplayName("should delete AVRO schema") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(187) - @Test - @Order(4) - void deleteSchemaAvro() { - navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName()); - schemaDetails - .removeSchema(); - schemaRegistryList - .waitUntilScreenReady(); - Assertions.assertFalse(schemaRegistryList.isSchemaVisible(AVRO_API.getName()),"isSchemaVisible()"); - SCHEMA_LIST.remove(AVRO_API); - } - - @DisplayName("should create JSON schema") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(89) - @Test - @Order(5) - void createSchemaJson() { - Schema schemaJson = Schema.createSchemaJson(); - navigateToSchemaRegistry(); - schemaRegistryList - .clickCreateSchema(); - schemaCreateForm - .setSubjectName(schemaJson.getName()) - .setSchemaField(fileToString(schemaJson.getValuePath())) - .selectSchemaTypeFromDropdown(schemaJson.getType()) - .clickSubmitButton(); - schemaDetails - .waitUntilScreenReady(); - SoftAssertions softly = new SoftAssertions(); - softly.assertThat(schemaDetails.isSchemaHeaderVisible(schemaJson.getName())).as("isSchemaHeaderVisible()").isTrue(); - softly.assertThat(schemaDetails.getSchemaType()).as("getSchemaType()").isEqualTo(schemaJson.getType().getValue()); - softly.assertThat(schemaDetails.getCompatibility()).as("getCompatibility()").isEqualTo(CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue()); - softly.assertAll(); - navigateToSchemaRegistry(); - Assertions.assertTrue(schemaRegistryList.isSchemaVisible(JSON_API.getName()),"isSchemaVisible()"); - SCHEMA_LIST.add(schemaJson); - } - - @DisplayName("should delete JSON schema") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(189) - @Test - @Order(6) - void deleteSchemaJson() { - navigateToSchemaRegistryAndOpenDetails(JSON_API.getName()); - schemaDetails - .removeSchema(); - schemaRegistryList - .waitUntilScreenReady(); - Assertions.assertFalse(schemaRegistryList.isSchemaVisible(JSON_API.getName()),"isSchemaVisible()"); - SCHEMA_LIST.remove(JSON_API); - } - - @DisplayName("should create PROTOBUF schema") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(91) - @Test - @Order(7) - void createSchemaProtobuf() { - Schema schemaProtobuf = Schema.createSchemaProtobuf(); - navigateToSchemaRegistry(); - schemaRegistryList - .clickCreateSchema(); - schemaCreateForm - .setSubjectName(schemaProtobuf.getName()) - .setSchemaField(fileToString(schemaProtobuf.getValuePath())) - .selectSchemaTypeFromDropdown(schemaProtobuf.getType()) - .clickSubmitButton(); - schemaDetails - .waitUntilScreenReady(); - SoftAssertions softly = new SoftAssertions(); - softly.assertThat(schemaDetails.isSchemaHeaderVisible(schemaProtobuf.getName())).as("isSchemaHeaderVisible()").isTrue(); - softly.assertThat(schemaDetails.getSchemaType()).as("getSchemaType()").isEqualTo(schemaProtobuf.getType().getValue()); - softly.assertThat(schemaDetails.getCompatibility()).as("getCompatibility()").isEqualTo(CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue()); - softly.assertAll(); - navigateToSchemaRegistry(); - Assertions.assertTrue(schemaRegistryList.isSchemaVisible(PROTOBUF_API.getName()),"isSchemaVisible()"); - SCHEMA_LIST.add(schemaProtobuf); - } - - @DisplayName("should delete PROTOBUF schema") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(223) - @Test - @Order(8) - void deleteSchemaProtobuf() { - navigateToSchemaRegistryAndOpenDetails(PROTOBUF_API.getName()); - schemaDetails - .removeSchema(); - schemaRegistryList - .waitUntilScreenReady(); - Assertions.assertFalse(schemaRegistryList.isSchemaVisible(PROTOBUF_API.getName()),"isSchemaVisible()"); - SCHEMA_LIST.remove(PROTOBUF_API); - } - - @AfterAll - public void afterAll() { - SCHEMA_LIST.forEach(schema -> apiService.deleteSchema(schema.getName())); - } - - @Step - private void navigateToSchemaRegistry(){ - naviSideBar - .openSideMenu(SCHEMA_REGISTRY); - schemaRegistryList - .waitUntilScreenReady(); - } - - @Step - private void navigateToSchemaRegistryAndOpenDetails(String schemaName){ - navigateToSchemaRegistry(); - schemaRegistryList - .openSchema(schemaName); - schemaDetails - .waitUntilScreenReady(); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/topics/TopicMessagesTests.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/topics/TopicMessagesTests.java deleted file mode 100644 index 205707a263..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/topics/TopicMessagesTests.java +++ /dev/null @@ -1,280 +0,0 @@ -package com.provectus.kafka.ui.suite.topics; - -import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; -import static com.provectus.kafka.ui.pages.topic.TopicDetails.TopicMenu.MESSAGES; -import static com.provectus.kafka.ui.pages.topic.TopicDetails.TopicMenu.OVERVIEW; -import static com.provectus.kafka.ui.utilities.TimeUtils.waitUntilNewMinuteStarted; -import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; -import static org.assertj.core.api.Assertions.assertThat; - -import com.provectus.kafka.ui.base.BaseTest; -import com.provectus.kafka.ui.models.Topic; -import com.provectus.kafka.ui.pages.topic.TopicDetails; -import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.AutomationStatus; -import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.Suite; -import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status; -import io.qameta.allure.Issue; -import io.qase.api.annotation.CaseId; -import io.qase.api.annotation.Step; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class TopicMessagesTests extends BaseTest { - private static final long SUITE_ID = 2; - private static final String SUITE_TITLE = "Topics"; - private static final Topic TOPIC_FOR_MESSAGES = new Topic() - .setName("topic-with-clean-message-attribute-" + randomAlphabetic(5)) - .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); - private static final Topic TOPIC_TO_CLEAR_MESSAGES = new Topic() - .setName("topic-to-clear-message-attribute-" + randomAlphabetic(5)) - .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); - private static final Topic TOPIC_FOR_CHECKING_FILTERS = new Topic() - .setName("topic-for-checking-filters-" + randomAlphabetic(5)) - .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); - private static final Topic TOPIC_TO_RECREATE = new Topic() - .setName("topic-to-recreate-attribute-" + randomAlphabetic(5)) - .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); - private static final List TOPIC_LIST = new ArrayList<>(); - - @BeforeAll - public void beforeAll() { - TOPIC_LIST.addAll(List.of(TOPIC_FOR_MESSAGES, TOPIC_FOR_CHECKING_FILTERS, TOPIC_TO_CLEAR_MESSAGES, TOPIC_TO_RECREATE)); - TOPIC_LIST.forEach(topic -> apiService.createTopic(topic.getName())); - IntStream.range(1, 3).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECKING_FILTERS)); - waitUntilNewMinuteStarted(); - IntStream.range(1, 3).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECKING_FILTERS)); - } - - @DisplayName("produce message") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(222) - @Order(1) - @Test - void produceMessage() { - navigateToTopicsAndOpenDetails(TOPIC_FOR_MESSAGES.getName()); - topicDetails - .openDetailsTab(MESSAGES); - produceMessage(TOPIC_FOR_MESSAGES); - SoftAssertions softly = new SoftAssertions(); - softly.assertThat(topicDetails.isKeyMessageVisible((TOPIC_FOR_MESSAGES.getMessageKey()))) - .withFailMessage("isKeyMessageVisible()").isTrue(); - softly.assertThat(topicDetails.isContentMessageVisible((TOPIC_FOR_MESSAGES.getMessageContent()).trim())) - .withFailMessage("isContentMessageVisible()").isTrue(); - softly.assertAll(); - } - - @Disabled - @Issue("https://github.com/provectus/kafka-ui/issues/2778") - @DisplayName("clear message") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(19) - @Order(2) - @Test - void clearMessage() { - navigateToTopicsAndOpenDetails(TOPIC_FOR_MESSAGES.getName()); - topicDetails - .openDetailsTab(OVERVIEW); - int messageAmount = topicDetails.getMessageCountAmount(); - produceMessage(TOPIC_FOR_MESSAGES); - Assertions.assertEquals(messageAmount + 1, topicDetails.getMessageCountAmount(), "getMessageCountAmount()"); - topicDetails - .openDotMenu() - .clickClearMessagesMenu() - .waitUntilScreenReady(); - Assertions.assertEquals(0, topicDetails.getMessageCountAmount(), "getMessageCountAmount()"); - } - - @DisplayName("TopicTests.clearMessageOfTopic : Clear message of topic") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(239) - @Order(3) - @Test - void checkClearTopicMessage() { - navigateToTopicsAndOpenDetails(TOPIC_TO_CLEAR_MESSAGES.getName()); - topicDetails - .openDetailsTab(OVERVIEW); - produceMessage(TOPIC_TO_CLEAR_MESSAGES); - navigateToTopics(); - assertThat(topicsList.getTopicItem(TOPIC_TO_CLEAR_MESSAGES.getName()).getNumberOfMessages()) - .as("getNumberOfMessages()").isEqualTo(1); - topicsList - .openDotMenuByTopicName(TOPIC_TO_CLEAR_MESSAGES.getName()) - .clickClearMessagesBtn() - .clickConfirmBtnMdl(); - SoftAssertions softly = new SoftAssertions(); - softly.assertThat(topicsList.isAlertWithMessageVisible(SUCCESS, - String.format("%s messages have been successfully cleared!", TOPIC_TO_CLEAR_MESSAGES.getName()))) - .as("isAlertWithMessageVisible()").isTrue(); - softly.assertThat(topicsList.getTopicItem(TOPIC_TO_CLEAR_MESSAGES.getName()).getNumberOfMessages()) - .as("getNumberOfMessages()").isEqualTo(0); - softly.assertAll(); - } - - @Disabled - @Issue("https://github.com/provectus/kafka-ui/issues/2819") - @DisplayName("Message copy from topic profile") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(21) - @Order(4) - @Test - void copyMessageFromTopicProfile() { - navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECKING_FILTERS.getName()); - topicDetails - .openDetailsTab(MESSAGES) - .getRandomMessage() - .openDotMenu() - .clickCopyToClipBoard(); - Assertions.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, "Copied successfully!"), - "isAlertWithMessageVisible()"); - } - - @Disabled - @Issue("https://github.com/provectus/kafka-ui/issues/2394") - @DisplayName("Checking messages filtering by Offset within Topic/Messages") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(15) - @Order(5) - @Test - void checkingMessageFilteringByOffset() { - navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECKING_FILTERS.getName()); - topicDetails - .openDetailsTab(MESSAGES); - TopicDetails.MessageGridItem secondMessage = topicDetails.getMessageByOffset(1); - topicDetails - .selectSeekTypeDdlMessagesTab("Offset") - .setSeekTypeValueFldMessagesTab(String.valueOf(secondMessage.getOffset())) - .clickSubmitFiltersBtnMessagesTab(); - SoftAssertions softly = new SoftAssertions(); - topicDetails.getAllMessages().forEach(message -> - softly.assertThat(message.getOffset() == secondMessage.getOffset() - || message.getOffset() > secondMessage.getOffset()) - .as(String.format("Expected offset is: %s, but found: %s", secondMessage.getOffset(), message.getOffset())) - .isTrue()); - softly.assertAll(); - } - - @Disabled - @Issue("https://github.com/provectus/kafka-ui/issues/3215") - @Issue("https://github.com/provectus/kafka-ui/issues/2345") - @DisplayName("Checking messages filtering by Timestamp within Messages/Topic") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(16) - @Order(6) - @Test - void checkingMessageFilteringByTimestamp() { - navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECKING_FILTERS.getName()); - topicDetails - .openDetailsTab(MESSAGES); - LocalDateTime firstTimestamp = topicDetails.getMessageByOffset(0).getTimestamp(); - List nextMessages = topicDetails.getAllMessages().stream() - .filter(message -> message.getTimestamp().getMinute() != firstTimestamp.getMinute()) - .collect(Collectors.toList()); - LocalDateTime nextTimestamp = Objects.requireNonNull(nextMessages.stream() - .findFirst().orElse(null)).getTimestamp(); - topicDetails - .selectSeekTypeDdlMessagesTab("Timestamp") - .openCalendarSeekType() - .selectDateAndTimeByCalendar(nextTimestamp) - .clickSubmitFiltersBtnMessagesTab(); - SoftAssertions softly = new SoftAssertions(); - topicDetails.getAllMessages().forEach(message -> - softly.assertThat(message.getTimestamp().isEqual(nextTimestamp) - || message.getTimestamp().isAfter(nextTimestamp)) - .as(String.format("Expected timestamp is: %s, but found: %s", nextTimestamp, message.getTimestamp())) - .isTrue()); - softly.assertAll(); - } - - @Disabled - @Issue("https://github.com/provectus/kafka-ui/issues/2778") - @DisplayName("Clear message of topic from topic profile") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(246) - @Order(7) - @Test - void checkClearTopicMessageFromOverviewTab() { - navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECKING_FILTERS.getName()); - topicDetails - .openDetailsTab(OVERVIEW) - .openDotMenu() - .clickClearMessagesMenu() - .clickConfirmBtnMdl(); - SoftAssertions softly = new SoftAssertions(); - softly.assertThat(topicDetails.isAlertWithMessageVisible(SUCCESS, - String.format("%s messages have been successfully cleared!", TOPIC_FOR_CHECKING_FILTERS.getName()))) - .as("isAlertWithMessageVisible()").isTrue(); - softly.assertThat(topicDetails.getMessageCountAmount()) - .as("getMessageCountAmount()= " + topicDetails.getMessageCountAmount()).isEqualTo(0); - softly.assertAll(); - } - - @DisplayName("TopicTests.recreateTopic : Recreate topic") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(240) - @Order(8) - @Test - void checkRecreateTopic(){ - navigateToTopicsAndOpenDetails(TOPIC_TO_RECREATE.getName()); - topicDetails - .openDetailsTab(OVERVIEW); - produceMessage(TOPIC_TO_RECREATE); - navigateToTopics(); - assertThat(topicsList.getTopicItem(TOPIC_TO_RECREATE.getName()).getNumberOfMessages()) - .as("getNumberOfMessages()").isEqualTo(1); - topicsList - .openDotMenuByTopicName(TOPIC_TO_RECREATE.getName()) - .clickRecreateTopicBtn() - .clickConfirmBtnMdl(); - SoftAssertions softly = new SoftAssertions(); - softly.assertThat(topicDetails.isAlertWithMessageVisible(SUCCESS, - String.format("Topic %s successfully recreated!", TOPIC_TO_RECREATE.getName()))) - .as("isAlertWithMessageVisible()").isTrue(); - softly.assertThat(topicsList.getTopicItem(TOPIC_TO_RECREATE.getName()).getNumberOfMessages()) - .as("getNumberOfMessages()").isEqualTo(0); - softly.assertAll(); - } - - @Step - protected void produceMessage(Topic topic){ - topicDetails - .clickProduceMessageBtn(); - produceMessagePanel - .waitUntilScreenReady() - .setKeyField(topic.getMessageKey()) - .setContentFiled(topic.getMessageContent()) - .submitProduceMessage(); - topicDetails - .waitUntilScreenReady(); - } - - @AfterAll - public void afterAll() { - TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/topics/TopicsTests.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/topics/TopicsTests.java deleted file mode 100644 index b70daa3307..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/topics/TopicsTests.java +++ /dev/null @@ -1,528 +0,0 @@ -package com.provectus.kafka.ui.suite.topics; - -import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; -import static com.provectus.kafka.ui.pages.topic.TopicDetails.TopicMenu.MESSAGES; -import static com.provectus.kafka.ui.pages.topic.TopicDetails.TopicMenu.SETTINGS; -import static com.provectus.kafka.ui.pages.topic.enums.CleanupPolicyValue.COMPACT; -import static com.provectus.kafka.ui.pages.topic.enums.CleanupPolicyValue.DELETE; -import static com.provectus.kafka.ui.pages.topic.enums.CustomParameterType.COMPRESSION_TYPE; -import static com.provectus.kafka.ui.pages.topic.enums.MaxSizeOnDisk.NOT_SET; -import static com.provectus.kafka.ui.pages.topic.enums.MaxSizeOnDisk.SIZE_1_GB; -import static com.provectus.kafka.ui.pages.topic.enums.MaxSizeOnDisk.SIZE_20_GB; -import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; -import static org.apache.commons.lang3.RandomUtils.nextInt; -import static org.assertj.core.api.Assertions.assertThat; - -import com.codeborne.selenide.Condition; -import com.provectus.kafka.ui.base.BaseTest; -import com.provectus.kafka.ui.models.Topic; -import com.provectus.kafka.ui.pages.topic.TopicDetails; -import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.AutomationStatus; -import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.Suite; -import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status; -import io.qameta.allure.Issue; -import io.qase.api.annotation.CaseId; -import java.util.ArrayList; -import java.util.List; -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.TestMethodOrder; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class TopicsTests extends BaseTest { - private static final long SUITE_ID = 2; - private static final String SUITE_TITLE = "Topics"; - private static final Topic TOPIC_TO_CREATE = new Topic() - .setName("new-topic-" + randomAlphabetic(5)) - .setNumberOfPartitions(1) - .setCustomParameterType(COMPRESSION_TYPE) - .setCustomParameterValue("producer") - .setCleanupPolicyValue(DELETE); - private static final Topic TOPIC_TO_UPDATE_AND_DELETE = new Topic() - .setName("topic-to-update-and-delete-" + randomAlphabetic(5)) - .setNumberOfPartitions(1) - .setCleanupPolicyValue(COMPACT) - .setTimeToRetainData("604800001") - .setMaxSizeOnDisk(SIZE_20_GB) - .setMaxMessageBytes("1000020") - .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); - private static final Topic TOPIC_TO_CHECK_SETTINGS = new Topic() - .setName("new-topic-" + randomAlphabetic(5)) - .setNumberOfPartitions(1) - .setMaxMessageBytes("1000012") - .setMaxSizeOnDisk(NOT_SET); - private static final Topic TOPIC_FOR_DELETE = new Topic().setName("topic-to-delete-" + randomAlphabetic(5)); - private static final List TOPIC_LIST = new ArrayList<>(); - - @BeforeAll - public void beforeAll() { - TOPIC_LIST.addAll(List.of(TOPIC_TO_UPDATE_AND_DELETE, TOPIC_FOR_DELETE)); - TOPIC_LIST.forEach(topic -> apiService.createTopic(topic.getName())); - } - - @DisplayName("should create a topic") - @Suite(suiteId = 4, title = "Create new Topic") - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(199) - @Test - @Order(1) - public void createTopic() { - navigateToTopics(); - topicsList - .clickAddTopicBtn(); - topicCreateEditForm - .waitUntilScreenReady() - .setTopicName(TOPIC_TO_CREATE.getName()) - .setNumberOfPartitions(TOPIC_TO_CREATE.getNumberOfPartitions()) - .selectCleanupPolicy(TOPIC_TO_CREATE.getCleanupPolicyValue()) - .clickCreateTopicBtn(); - navigateToTopicsAndOpenDetails(TOPIC_TO_CREATE.getName()); - SoftAssertions softly = new SoftAssertions(); - softly.assertThat(topicDetails.isTopicHeaderVisible(TOPIC_TO_CREATE.getName())).as("isTopicHeaderVisible()") - .isTrue(); - softly.assertThat(topicDetails.getCleanUpPolicy()).as("getCleanUpPolicy()") - .isEqualTo(TOPIC_TO_CREATE.getCleanupPolicyValue().toString()); - softly.assertThat(topicDetails.getPartitions()).as("getPartitions()") - .isEqualTo(TOPIC_TO_CREATE.getNumberOfPartitions()); - softly.assertAll(); - navigateToTopics(); - Assertions.assertTrue(topicsList.isTopicVisible(TOPIC_TO_CREATE.getName()), "isTopicVisible"); - TOPIC_LIST.add(TOPIC_TO_CREATE); - } - - @DisplayName("Checking available operations for selected Topic within 'All Topics' page") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(7) - @Test - @Order(2) - void checkAvailableOperations() { - navigateToTopics(); - topicsList - .getTopicItem("my_ksql_1ksql_processing_log") - .selectItem(true); - verifyElementsCondition(topicsList.getActionButtons(),Condition.enabled); - topicsList - .getTopicItem("_confluent-ksql-my_ksql_1_command_topic") - .selectItem(true); - Assertions.assertFalse(topicsList.isCopySelectedTopicBtnEnabled(), "isCopySelectedTopicBtnEnabled()"); - } - - @Disabled() - @Issue("https://github.com/provectus/kafka-ui/issues/2625") - @DisplayName("should update a topic") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(197) - @Test - @Order(3) - public void updateTopic() { - navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName()); - topicDetails - .openDotMenu() - .clickEditSettingsMenu(); - topicCreateEditForm - .waitUntilScreenReady() - .selectCleanupPolicy((TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue())) - .setMinInsyncReplicas(10) - .setTimeToRetainDataInMs(TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetainData()) - .setMaxSizeOnDiskInGB(TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk()) - .setMaxMessageBytes(TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes()) - .clickCreateTopicBtn(); - topicDetails - .waitUntilScreenReady(); - navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName()); - topicDetails - .openDotMenu() - .clickEditSettingsMenu(); - SoftAssertions softly = new SoftAssertions(); - softly.assertThat(topicCreateEditForm.getCleanupPolicy()).as("getCleanupPolicy()") - .isEqualTo(TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue().getVisibleText()); - softly.assertThat(topicCreateEditForm.getTimeToRetain()).as("getTimeToRetain()") - .isEqualTo(TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetainData()); - softly.assertThat(topicCreateEditForm.getMaxSizeOnDisk()).as("getMaxSizeOnDisk()") - .isEqualTo(TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk().getVisibleText()); - softly.assertThat(topicCreateEditForm.getMaxMessageBytes()).as("getMaxMessageBytes()") - .isEqualTo(TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes()); - softly.assertAll(); - } - - @DisplayName("TopicTests.removeTopicFromAllTopics : Remove topic from 'all topics'/'TopicList'") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(242) - @Test - @Order(4) - public void removeTopicFromTopicList() { - navigateToTopics(); - topicsList - .openDotMenuByTopicName(TOPIC_TO_UPDATE_AND_DELETE.getName()) - .clickRemoveTopicBtn() - .clickConfirmBtnMdl(); - Assertions.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS, - String.format("Topic %s successfully deleted!", TOPIC_TO_UPDATE_AND_DELETE.getName())), - "isAlertWithMessageVisible()"); - TOPIC_LIST.remove(TOPIC_TO_UPDATE_AND_DELETE); - } - - @DisplayName("should delete topic") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(207) - @Test - @Order(5) - public void deleteTopic() { - navigateToTopicsAndOpenDetails(TOPIC_FOR_DELETE.getName()); - topicDetails - .openDotMenu() - .clickDeleteTopicMenu() - .clickConfirmBtnMdl(); - navigateToTopics(); - Assertions.assertFalse(topicsList.isTopicVisible(TOPIC_FOR_DELETE.getName()), "isTopicVisible"); - TOPIC_LIST.remove(TOPIC_FOR_DELETE); - } - - @DisplayName("Redirect to consumer from topic profile") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(20) - @Test - @Order(6) - void redirectToConsumerFromTopic() { - String topicName = "source-activities"; - String consumerGroupId = "connect-sink_postgres_activities"; - navigateToTopicsAndOpenDetails(topicName); - topicDetails - .openDetailsTab(TopicDetails.TopicMenu.CONSUMERS) - .openConsumerGroup(consumerGroupId); - consumersDetails - .waitUntilScreenReady(); - assertThat(consumersDetails.isRedirectedConsumerTitleVisible(consumerGroupId)) - .withFailMessage("isRedirectedConsumerTitleVisible").isTrue(); - assertThat(consumersDetails.isTopicInConsumersDetailsVisible(topicName)) - .withFailMessage("isTopicInConsumersDetailsVisible").isTrue(); - } - - @DisplayName("Checking Topic creation possibility in case of empty Topic Name") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(4) - @Test - @Order(7) - void checkTopicCreatePossibility() { - navigateToTopics(); - topicsList - .clickAddTopicBtn(); - topicCreateEditForm - .waitUntilScreenReady(); - assertThat(topicCreateEditForm.isCreateTopicButtonEnabled()).as("isCreateTopicButtonEnabled()").isFalse(); - topicCreateEditForm - .setTopicName("testName"); - assertThat(topicCreateEditForm.isCreateTopicButtonEnabled()).as("isCreateTopicButtonEnabled()").isFalse(); - topicCreateEditForm - .setTopicName(null) - .setNumberOfPartitions(nextInt(1, 10)); - assertThat(topicCreateEditForm.isCreateTopicButtonEnabled()).as("isCreateTopicButtonEnabled()").isFalse(); - topicCreateEditForm - .setTopicName("testName"); - assertThat(topicCreateEditForm.isCreateTopicButtonEnabled()).as("isCreateTopicButtonEnabled()").isTrue(); - } - - @DisplayName("Checking 'Time to retain data (in ms)' custom value with editing Topic's settings") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(266) - @Test - @Order(8) - void checkTimeToRetainDataCustomValueWithEditingTopic() { - Topic topicToRetainData = new Topic() - .setName("topic-to-retain-data-" + randomAlphabetic(5)) - .setTimeToRetainData("86400000"); - navigateToTopics(); - topicsList - .clickAddTopicBtn(); - topicCreateEditForm - .waitUntilScreenReady() - .setTopicName(topicToRetainData.getName()) - .setNumberOfPartitions(1) - .setTimeToRetainDataInMs("604800000"); - assertThat(topicCreateEditForm.getTimeToRetain()).as("getTimeToRetain()").isEqualTo("604800000"); - topicCreateEditForm - .setTimeToRetainDataInMs(topicToRetainData.getTimeToRetainData()) - .clickCreateTopicBtn(); - topicDetails - .waitUntilScreenReady() - .openDotMenu() - .clickEditSettingsMenu(); - assertThat(topicCreateEditForm.getTimeToRetain()).as("getTimeToRetain()") - .isEqualTo(topicToRetainData.getTimeToRetainData()); - topicDetails - .openDetailsTab(SETTINGS); - assertThat(topicDetails.getSettingsGridValueByKey("retention.ms")).as("getSettingsGridValueByKey()") - .isEqualTo(topicToRetainData.getTimeToRetainData()); - TOPIC_LIST.add(topicToRetainData); - } - - @DisplayName("Checking requiredness of Custom parameters within 'Create new Topic'") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(6) - @Test - @Order(9) - void checkCustomParametersWithinCreateNewTopic() { - navigateToTopics(); - topicsList - .clickAddTopicBtn(); - topicCreateEditForm - .waitUntilScreenReady() - .setTopicName(TOPIC_TO_CREATE.getName()) - .clickAddCustomParameterTypeButton() - .setCustomParameterType(TOPIC_TO_CREATE.getCustomParameterType()); - assertThat(topicCreateEditForm.isDeleteCustomParameterButtonEnabled()).as("isDeleteCustomParameterButtonEnabled()") - .isTrue(); - topicCreateEditForm - .clearCustomParameterValue(); - assertThat(topicCreateEditForm.isValidationMessageCustomParameterValueVisible()) - .as("isValidationMessageCustomParameterValueVisible()").isTrue(); - } - - @DisplayName("Checking Topics section within Kafka-ui Application") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(2) - @Test - @Order(10) - void checkTopicListElements() { - navigateToTopics(); - verifyElementsCondition(topicsList.getAllVisibleElements(), Condition.visible); - verifyElementsCondition(topicsList.getAllEnabledElements(), Condition.enabled); - } - - @DisplayName("Filter adding within Topic") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(12) - @Test - @Order(11) - void addingNewFilterWithinTopic() { - String filterName = randomAlphabetic(5); - navigateToTopicsAndOpenDetails("_schemas"); - topicDetails - .openDetailsTab(MESSAGES) - .clickMessagesAddFiltersBtn() - .waitUntilAddFiltersMdlVisible(); - verifyElementsCondition(topicDetails.getAllAddFilterModalVisibleElements(), Condition.visible); - verifyElementsCondition(topicDetails.getAllAddFilterModalEnabledElements(), Condition.enabled); - verifyElementsCondition(topicDetails.getAllAddFilterModalDisabledElements(), Condition.disabled); - assertThat(topicDetails.isSaveThisFilterCheckBoxSelected()).as("isSaveThisFilterCheckBoxSelected()") - .isFalse(); - topicDetails - .setFilterCodeFieldAddFilterMdl(filterName); - assertThat(topicDetails.isAddFilterBtnAddFilterMdlEnabled()).as("isAddFilterBtnAddFilterMdlEnabled()") - .isTrue(); - topicDetails.clickAddFilterBtnAndCloseMdl(true); - assertThat(topicDetails.isActiveFilterVisible(filterName)).as("isActiveFilterVisible()") - .isTrue(); - } - - @DisplayName("Checking filter saving within Messages/Topic profile/Saved Filters") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(13) - @Test - @Order(12) - void checkFilterSavingWithinSavedFilters() { - String displayName = randomAlphabetic(5); - navigateToTopicsAndOpenDetails("my_ksql_1ksql_processing_log"); - topicDetails - .openDetailsTab(MESSAGES) - .clickMessagesAddFiltersBtn() - .waitUntilAddFiltersMdlVisible() - .setFilterCodeFieldAddFilterMdl(randomAlphabetic(4)) - .selectSaveThisFilterCheckboxMdl(true) - .setDisplayNameFldAddFilterMdl(displayName); - assertThat(topicDetails.isAddFilterBtnAddFilterMdlEnabled()).as("isAddFilterBtnAddFilterMdlEnabled()") - .isTrue(); - topicDetails - .clickAddFilterBtnAndCloseMdl(false) - .openSavedFiltersListMdl(); - assertThat(topicDetails.isFilterVisibleAtSavedFiltersMdl(displayName)) - .as("isFilterVisibleAtSavedFiltersMdl()").isTrue(); - } - - @DisplayName("Checking applying saved filter within Topic/Messages") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(14) - @Test - @Order(13) - void checkingApplyingSavedFilterWithinTopicMessages() { - String displayName = randomAlphabetic(5); - navigateToTopicsAndOpenDetails("my_ksql_1ksql_processing_log"); - topicDetails - .openDetailsTab(MESSAGES) - .clickMessagesAddFiltersBtn() - .waitUntilAddFiltersMdlVisible() - .setFilterCodeFieldAddFilterMdl(randomAlphabetic(4)) - .selectSaveThisFilterCheckboxMdl(true) - .setDisplayNameFldAddFilterMdl(displayName) - .clickAddFilterBtnAndCloseMdl(false) - .openSavedFiltersListMdl() - .selectFilterAtSavedFiltersMdl(displayName) - .clickSelectFilterBtnAtSavedFiltersMdl(); - assertThat(topicDetails.isActiveFilterVisible(displayName)) - .as("isActiveFilterVisible()").isTrue(); - } - - @DisplayName("Checking 'Show Internal Topics' toggle functionality within 'All Topics' page") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(11) - @Test - @Order(14) - void checkShowInternalTopicsButtonFunctionality(){ - navigateToTopics(); - SoftAssertions softly = new SoftAssertions(); - softly.assertThat(topicsList.isShowInternalRadioBtnSelected()).as("isInternalRadioBtnSelected()").isTrue(); - softly.assertThat(topicsList.getInternalTopics()).as("getInternalTopics()").size().isGreaterThan(0); - softly.assertThat(topicsList.getNonInternalTopics()).as("getNonInternalTopics()").size().isGreaterThan(0); - softly.assertAll(); - topicsList - .setShowInternalRadioButton(false); - softly.assertThat(topicsList.getInternalTopics()).as("getInternalTopics()").size().isEqualTo(0); - softly.assertThat(topicsList.getNonInternalTopics()).as("getNonInternalTopics()").size().isGreaterThan(0); - softly.assertAll(); - } - - @DisplayName("Checking Topics settings to make sure retention.bytes is right according to Max size on disk in GB selected value") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(56) - @Test - @Order(15) - void checkRetentionBytesAccordingToMaxSizeOnDisk(){ - navigateToTopics(); - topicsList - .clickAddTopicBtn(); - topicCreateEditForm - .waitUntilScreenReady() - .setTopicName(TOPIC_TO_CHECK_SETTINGS.getName()) - .setNumberOfPartitions(TOPIC_TO_CHECK_SETTINGS.getNumberOfPartitions()) - .setMaxMessageBytes(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes()) - .clickCreateTopicBtn(); - topicDetails - .waitUntilScreenReady(); - TOPIC_LIST.add(TOPIC_TO_CHECK_SETTINGS); - topicDetails - .openDetailsTab(SETTINGS); - topicSettingsTab - .waitUntilScreenReady(); - SoftAssertions softly = new SoftAssertions(); - softly.assertThat(topicSettingsTab.getValueByKey("retention.bytes")) - .as("getValueOfKey(retention.bytes)").isEqualTo(TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk().getOptionValue()); - softly.assertThat(topicSettingsTab.getValueByKey("max.message.bytes")) - .as("getValueOfKey(max.message.bytes)").isEqualTo(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes()); - softly.assertAll(); - TOPIC_TO_CHECK_SETTINGS - .setMaxSizeOnDisk(SIZE_1_GB) - .setMaxMessageBytes("1000056"); - topicDetails - .openDotMenu() - .clickEditSettingsMenu(); - topicCreateEditForm - .waitUntilScreenReady() - .setMaxSizeOnDiskInGB(TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk()) - .setMaxMessageBytes(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes()) - .clickCreateTopicBtn(); - topicDetails - .waitUntilScreenReady() - .openDetailsTab(SETTINGS); - topicSettingsTab - .waitUntilScreenReady(); - softly.assertThat(topicSettingsTab.getValueByKey("retention.bytes")) - .as("getValueOfKey(retention.bytes)").isEqualTo(TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk().getOptionValue()); - softly.assertThat(topicSettingsTab.getValueByKey("max.message.bytes")) - .as("getValueOfKey(max.message.bytes)").isEqualTo(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes()); - softly.assertAll(); - } - - @DisplayName("TopicTests.recreateTopicFromTopicProfile : Recreate topic from topic profile") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(247) - @Test - @Order(16) - void recreateTopicFromTopicProfile(){ - Topic topicToRecreate = new Topic() - .setName("topic-to-recreate-" + randomAlphabetic(5)) - .setNumberOfPartitions(1); - navigateToTopics(); - topicsList - .clickAddTopicBtn(); - topicCreateEditForm - .waitUntilScreenReady() - .setTopicName(topicToRecreate.getName()) - .setNumberOfPartitions(topicToRecreate.getNumberOfPartitions()) - .clickCreateTopicBtn(); - topicDetails - .waitUntilScreenReady(); - TOPIC_LIST.add(topicToRecreate); - topicDetails - .openDotMenu() - .clickRecreateTopicMenu(); - assertThat(topicDetails.isConfirmationMdlVisible()).as("isConfirmationMdlVisible()").isTrue(); - topicDetails - .clickConfirmBtnMdl(); - assertThat(topicDetails.isAlertWithMessageVisible(SUCCESS, - String.format("Topic %s successfully recreated!", topicToRecreate.getName()))) - .as("isAlertWithMessageVisible()").isTrue(); - } - - @DisplayName("TopicTests.copyTopic : Copy topic") - @Suite(suiteId = SUITE_ID, title = SUITE_TITLE) - @AutomationStatus(status = Status.AUTOMATED) - @CaseId(8) - @Test - @Order(17) - void checkCopyTopicPossibility(){ - Topic topicToCopy = new Topic() - .setName("topic-to-copy-" + randomAlphabetic(5)) - .setNumberOfPartitions(1); - navigateToTopics(); - topicsList - .getTopicItem("_schemas") - .selectItem(true) - .clickCopySelectedTopicBtn(); - topicCreateEditForm - .waitUntilScreenReady(); - assertThat(topicCreateEditForm.isCreateTopicButtonEnabled()).as("isCreateTopicButtonEnabled()").isFalse(); - topicCreateEditForm - .setTopicName(topicToCopy.getName()) - .setNumberOfPartitions(topicToCopy.getNumberOfPartitions()) - .clickCreateTopicBtn(); - topicDetails - .waitUntilScreenReady(); - TOPIC_LIST.add(topicToCopy); - SoftAssertions softly = new SoftAssertions(); - softly.assertThat(topicDetails.isAlertWithMessageVisible(SUCCESS, "Topic successfully created.")) - .as("isAlertWithMessageVisible()").isTrue(); - softly.assertThat(topicDetails.isTopicHeaderVisible(topicToCopy.getName())) - .as("isTopicHeaderVisible()").isTrue(); - softly.assertAll(); - } - - @AfterAll - public void afterAll() { - TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); - } -} diff --git a/kafka-ui-e2e-checks/src/test/resources/manual.xml b/kafka-ui-e2e-checks/src/test/resources/manual.xml new file mode 100644 index 0000000000..dff467651e --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/resources/manual.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/kafka-ui-e2e-checks/src/test/resources/qase.xml b/kafka-ui-e2e-checks/src/test/resources/qase.xml new file mode 100644 index 0000000000..2b5d023b1a --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/resources/qase.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/kafka-ui-e2e-checks/src/test/resources/regression.xml b/kafka-ui-e2e-checks/src/test/resources/regression.xml new file mode 100644 index 0000000000..01db95d03b --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/resources/regression.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/kafka-ui-e2e-checks/src/test/resources/sanity.xml b/kafka-ui-e2e-checks/src/test/resources/sanity.xml new file mode 100644 index 0000000000..e1c8e8a31f --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/resources/sanity.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/kafka-ui-e2e-checks/src/test/resources/smoke.xml b/kafka-ui-e2e-checks/src/test/resources/smoke.xml new file mode 100644 index 0000000000..e0a8f082e8 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/resources/smoke.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/kafka-ui-react-app/.babelrc b/kafka-ui-react-app/.babelrc deleted file mode 100644 index 202d425a09..0000000000 --- a/kafka-ui-react-app/.babelrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "presets": [ - "@babel/preset-env", - "@babel/preset-react", - "@babel/preset-typescript" - ] -} diff --git a/kafka-ui-react-app/.eslintrc.json b/kafka-ui-react-app/.eslintrc.json index 5116aad770..4d524ef20a 100644 --- a/kafka-ui-react-app/.eslintrc.json +++ b/kafka-ui-react-app/.eslintrc.json @@ -21,6 +21,7 @@ ] }, "plugins": [ + "react", "@typescript-eslint", "prettier", "react-hooks" @@ -31,6 +32,8 @@ "plugin:@typescript-eslint/recommended", "plugin:jest-dom/recommended", "plugin:prettier/recommended", + "eslint:recommended", + "plugin:react/recommended", "prettier" ], "rules": { @@ -83,7 +86,8 @@ "unnamedComponents": "arrow-function" } ], - "react/jsx-no-constructed-context-values": "off" + "react/jsx-no-constructed-context-values": "off", + "react/display-name": "off" }, "overrides": [ { diff --git a/kafka-ui-react-app/.husky/pre-commit b/kafka-ui-react-app/.husky/pre-commit deleted file mode 100755 index 9435e20433..0000000000 --- a/kafka-ui-react-app/.husky/pre-commit +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -if git diff --cached --name-only | grep --quiet "kafka-ui-react-app" -then - cd kafka-ui-react-app && npm run pre-commit -else - echo "Skipping frontend tests" - exit 0 -fi diff --git a/kafka-ui-react-app/README.md b/kafka-ui-react-app/README.md index 8ab814cd1d..a66c4d9459 100644 --- a/kafka-ui-react-app/README.md +++ b/kafka-ui-react-app/README.md @@ -46,7 +46,7 @@ VITE_DEV_PROXY= https://api.server # your API server Run the application ```sh -pnpm start +pnpm dev ``` ### Docker way @@ -62,7 +62,7 @@ Make sure that none of the `.env*` files contain `DEV_PROXY` variable Run the application ```sh -pnpm start +pnpm dev ``` ## Links diff --git a/kafka-ui-react-app/package.json b/kafka-ui-react-app/package.json index 5c51e70007..50ab162eca 100644 --- a/kafka-ui-react-app/package.json +++ b/kafka-ui-react-app/package.json @@ -4,10 +4,7 @@ "homepage": "./", "private": true, "dependencies": { - "@babel/core": "^7.16.0", - "@babel/plugin-syntax-flow": "^7.18.6", - "@babel/plugin-transform-react-jsx": "^7.18.6", - "@floating-ui/react-dom-interactions": "^0.10.3", + "@floating-ui/react": "^0.19.2", "@hookform/error-message": "^2.0.0", "@hookform/resolvers": "^2.7.1", "@microsoft/fetch-event-source": "^2.0.1", @@ -15,26 +12,26 @@ "@szhsin/react-menu": "^3.1.1", "@tanstack/react-query": "^4.0.5", "@tanstack/react-table": "^8.5.10", - "@testing-library/react": "^13.2.0", + "@testing-library/react": "^14.0.0", "@types/testing-library__jest-dom": "^5.14.5", "ace-builds": "^1.7.1", "ajv": "^8.6.3", "ajv-formats": "^2.1.1", - "babel-jest": "^29.0.3", "classnames": "^2.2.6", "fetch-mock": "^9.11.0", - "jest": "^29.0.3", - "jest-watch-typeahead": "^2.0.0", + "jest": "^29.4.3", + "jest-watch-typeahead": "^2.2.2", "json-schema-faker": "^0.5.0-rcv.44", + "jsonpath-plus": "^7.2.0", "lodash": "^4.17.21", "pretty-ms": "7.0.1", "react": "^18.1.0", "react-ace": "^10.1.0", - "react-datepicker": "^4.8.0", + "react-datepicker": "^4.10.0", "react-dom": "^18.1.0", "react-error-boundary": "^3.1.4", - "react-hook-form": "7.6.9", - "react-hot-toast": "^2.3.0", + "react-hook-form": "7.43.1", + "react-hot-toast": "^2.4.0", "react-is": "^18.2.0", "react-multi-select-component": "^4.3.3", "react-redux": "^8.0.2", @@ -42,21 +39,16 @@ "redux": "^4.2.0", "sass": "^1.52.3", "styled-components": "^5.3.1", - "use-debounce": "^8.0.1", + "use-debounce": "^9.0.3", "vite": "^4.0.0", "vite-tsconfig-paths": "^4.0.2", "whatwg-fetch": "^3.6.2", - "yup": "^0.32.11", + "yup": "^1.0.0", "zustand": "^4.1.1" }, - "lint-staged": { - "*.{ts,tsx}": [ - "eslint --fix", - "pnpm test -- --bail --findRelatedTests --watchAll=false --passWithNoTests" - ] - }, "scripts": { "start": "vite", + "dev": "vite", "gen:sources": "rimraf src/generated-sources && openapi-generator-cli generate", "build": "vite build", "preview": "vite preview", @@ -67,30 +59,21 @@ "test:coverage": "jest --watchAll --coverage", "test:CI": "CI=true pnpm test:coverage --ci --testResultsProcessor=\"jest-sonar-reporter\" --watchAll=false", "tsc": "tsc --pretty --noEmit", - "prepare": "cd .. && husky install kafka-ui-react-app/.husky", - "pre-commit": "pnpm tsc && lint-staged", "deadcode": "ts-prune -i src/generated-sources" }, - "eslintConfig": { - "extends": "react-app" - }, "devDependencies": { - "@babel/preset-env": "^7.18.2", - "@babel/preset-react": "^7.17.12", - "@babel/preset-typescript": "^7.17.12", - "@jest/types": "^29.0.3", - "@openapitools/openapi-generator-cli": "^2.5.1", - "@swc/core": "^1.3.22", + "@jest/types": "^29.4.3", + "@openapitools/openapi-generator-cli": "^2.5.2", + "@swc/core": "^1.3.36", "@swc/jest": "^0.2.24", - "@testing-library/dom": "^8.11.1", - "@testing-library/jest-dom": "^5.16.4", + "@testing-library/dom": "^9.0.0", + "@testing-library/jest-dom": "^5.16.5", "@testing-library/user-event": "^14.4.3", "@types/eventsource": "^1.1.8", - "@types/jest": "^29.0.1", "@types/lodash": "^4.14.172", "@types/node": "^16.4.13", "@types/react": "^18.0.9", - "@types/react-datepicker": "^4.4.2", + "@types/react-datepicker": "^4.8.0", "@types/react-dom": "^18.0.3", "@types/react-router-dom": "^5.3.3", "@types/styled-components": "^5.1.13", @@ -102,23 +85,20 @@ "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-prettier": "^8.5.0", - "eslint-config-react-app": "^7.0.1", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.2.7", "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest-dom": "^4.0.2", + "eslint-plugin-jest-dom": "^4.0.3", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.5.0", - "husky": "^8.0.1", - "jest-environment-jsdom": "^29.0.3", + "jest-environment-jsdom": "^29.4.3", "jest-sonar-reporter": "^2.0.0", - "jest-styled-components": "^7.0.8", - "lint-staged": "^13.0.2", - "prettier": "^2.3.1", - "rimraf": "^3.0.2", - "ts-node": "^10.8.1", + "jest-styled-components": "^7.1.1", + "prettier": "^2.8.4", + "rimraf": "^4.1.2", + "ts-node": "^10.9.1", "ts-prune": "^0.10.3", "typescript": "^4.7.4", "vite-plugin-ejs": "^1.6.4" diff --git a/kafka-ui-react-app/pnpm-lock.yaml b/kafka-ui-react-app/pnpm-lock.yaml index 52390df0cd..feb4221ede 100644 --- a/kafka-ui-react-app/pnpm-lock.yaml +++ b/kafka-ui-react-app/pnpm-lock.yaml @@ -1,34 +1,27 @@ lockfileVersion: 5.4 specifiers: - '@babel/core': ^7.16.0 - '@babel/plugin-syntax-flow': ^7.18.6 - '@babel/plugin-transform-react-jsx': ^7.18.6 - '@babel/preset-env': ^7.18.2 - '@babel/preset-react': ^7.17.12 - '@babel/preset-typescript': ^7.17.12 - '@floating-ui/react-dom-interactions': ^0.10.3 + '@floating-ui/react': ^0.19.2 '@hookform/error-message': ^2.0.0 '@hookform/resolvers': ^2.7.1 - '@jest/types': ^29.0.3 + '@jest/types': ^29.4.3 '@microsoft/fetch-event-source': ^2.0.1 - '@openapitools/openapi-generator-cli': ^2.5.1 + '@openapitools/openapi-generator-cli': ^2.5.2 '@reduxjs/toolkit': ^1.8.3 - '@swc/core': ^1.3.22 + '@swc/core': ^1.3.36 '@swc/jest': ^0.2.24 '@szhsin/react-menu': ^3.1.1 '@tanstack/react-query': ^4.0.5 '@tanstack/react-table': ^8.5.10 - '@testing-library/dom': ^8.11.1 - '@testing-library/jest-dom': ^5.16.4 - '@testing-library/react': ^13.2.0 + '@testing-library/dom': ^9.0.0 + '@testing-library/jest-dom': ^5.16.5 + '@testing-library/react': ^14.0.0 '@testing-library/user-event': ^14.4.3 '@types/eventsource': ^1.1.8 - '@types/jest': ^29.0.1 '@types/lodash': ^4.14.172 '@types/node': ^16.4.13 '@types/react': ^18.0.9 - '@types/react-datepicker': ^4.4.2 + '@types/react-datepicker': ^4.8.0 '@types/react-dom': ^18.0.3 '@types/react-router-dom': ^5.3.3 '@types/styled-components': ^5.1.13 @@ -39,92 +32,86 @@ specifiers: ace-builds: ^1.7.1 ajv: ^8.6.3 ajv-formats: ^2.1.1 - babel-jest: ^29.0.3 classnames: ^2.2.6 dotenv: ^16.0.1 eslint: ^8.3.0 eslint-config-airbnb: ^19.0.4 eslint-config-airbnb-typescript: ^17.0.0 eslint-config-prettier: ^8.5.0 - eslint-config-react-app: ^7.0.1 eslint-import-resolver-node: ^0.3.6 eslint-import-resolver-typescript: ^3.2.7 eslint-plugin-import: ^2.26.0 - eslint-plugin-jest-dom: ^4.0.2 + eslint-plugin-jest-dom: ^4.0.3 eslint-plugin-jsx-a11y: ^6.5.1 eslint-plugin-prettier: ^4.0.0 eslint-plugin-react: ^7.30.1 eslint-plugin-react-hooks: ^4.5.0 fetch-mock: ^9.11.0 - husky: ^8.0.1 - jest: ^29.0.3 - jest-environment-jsdom: ^29.0.3 + jest: ^29.4.3 + jest-environment-jsdom: ^29.4.3 jest-sonar-reporter: ^2.0.0 - jest-styled-components: ^7.0.8 - jest-watch-typeahead: ^2.0.0 + jest-styled-components: ^7.1.1 + jest-watch-typeahead: ^2.2.2 json-schema-faker: ^0.5.0-rcv.44 - lint-staged: ^13.0.2 + jsonpath-plus: ^7.2.0 lodash: ^4.17.21 - prettier: ^2.3.1 + prettier: ^2.8.4 pretty-ms: 7.0.1 react: ^18.1.0 react-ace: ^10.1.0 - react-datepicker: ^4.8.0 + react-datepicker: ^4.10.0 react-dom: ^18.1.0 react-error-boundary: ^3.1.4 - react-hook-form: 7.6.9 - react-hot-toast: ^2.3.0 + react-hook-form: 7.43.1 + react-hot-toast: ^2.4.0 react-is: ^18.2.0 react-multi-select-component: ^4.3.3 react-redux: ^8.0.2 react-router-dom: ^6.3.0 redux: ^4.2.0 - rimraf: ^3.0.2 + rimraf: ^4.1.2 sass: ^1.52.3 styled-components: ^5.3.1 - ts-node: ^10.8.1 + ts-node: ^10.9.1 ts-prune: ^0.10.3 typescript: ^4.7.4 - use-debounce: ^8.0.1 + use-debounce: ^9.0.3 vite: ^4.0.0 vite-plugin-ejs: ^1.6.4 vite-tsconfig-paths: ^4.0.2 whatwg-fetch: ^3.6.2 - yup: ^0.32.11 + yup: ^1.0.0 zustand: ^4.1.1 dependencies: - '@babel/core': 7.18.2 - '@babel/plugin-syntax-flow': 7.18.6_@babel+core@7.18.2 - '@babel/plugin-transform-react-jsx': 7.18.6_@babel+core@7.18.2 - '@floating-ui/react-dom-interactions': 0.10.3_ohobp6rpsmerwlq5ipwfh5yigy - '@hookform/error-message': 2.0.0_l2dcsysovzdujulgxvsen7vbsm - '@hookform/resolvers': 2.8.9_react-hook-form@7.6.9 + '@floating-ui/react': 0.19.2_ohobp6rpsmerwlq5ipwfh5yigy + '@hookform/error-message': 2.0.0_ie7afrsxjmqsdnv6azc5osehhy + '@hookform/resolvers': 2.8.9_react-hook-form@7.43.1 '@microsoft/fetch-event-source': 2.0.1 '@reduxjs/toolkit': 1.8.3_ctm756ikdwcjcvyfxxwskzbr6q '@szhsin/react-menu': 3.1.1_ef5jwxihqo6n7gxfmzogljlgcm '@tanstack/react-query': 4.0.5_ef5jwxihqo6n7gxfmzogljlgcm '@tanstack/react-table': 8.5.10_ef5jwxihqo6n7gxfmzogljlgcm - '@testing-library/react': 13.2.0_ef5jwxihqo6n7gxfmzogljlgcm + '@testing-library/react': 14.0.0_ef5jwxihqo6n7gxfmzogljlgcm '@types/testing-library__jest-dom': 5.14.5 ace-builds: 1.7.1 ajv: 8.8.2 - ajv-formats: 2.1.1_ajv@8.8.2 - babel-jest: 29.0.3_@babel+core@7.18.2 + ajv-formats: 2.1.1 classnames: 2.3.1 fetch-mock: 9.11.0 - jest: 29.0.3_yqiaopbgmqcuvx27p5xxvum6wm - jest-watch-typeahead: 2.0.0_jest@29.0.3 + jest: 29.5.0_6m7kcbkkzjz4ln6z66tlzx44we + jest-watch-typeahead: 2.2.2_jest@29.5.0 json-schema-faker: 0.5.0-rcv.44 + jsonpath-plus: 7.2.0 lodash: 4.17.21 pretty-ms: 7.0.1 react: 18.1.0 react-ace: 10.1.0_ef5jwxihqo6n7gxfmzogljlgcm - react-datepicker: 4.8.0_ef5jwxihqo6n7gxfmzogljlgcm + react-datepicker: 4.10.0_ef5jwxihqo6n7gxfmzogljlgcm react-dom: 18.1.0_react@18.1.0 react-error-boundary: 3.1.4_react@18.1.0 - react-hook-form: 7.6.9_react@18.1.0 - react-hot-toast: 2.3.0_ef5jwxihqo6n7gxfmzogljlgcm + react-hook-form: 7.43.1_react@18.1.0 + react-hot-toast: 2.4.0_ef5jwxihqo6n7gxfmzogljlgcm react-is: 18.2.0 react-multi-select-component: 4.3.3_ef5jwxihqo6n7gxfmzogljlgcm react-redux: 8.0.2_nfqigfgwurfoimtkde74cji6ga @@ -132,30 +119,26 @@ dependencies: redux: 4.2.0 sass: 1.52.3 styled-components: 5.3.1_uuaz5p7xzfmtjacf6iqf7idnby - use-debounce: 8.0.1_react@18.1.0 + use-debounce: 9.0.3_react@18.1.0 vite: 4.0.0_binvsr2w2vzqveeue6ibz2djda vite-tsconfig-paths: 4.0.2_eqmiqdrctagsk5ranq2vs4ssty whatwg-fetch: 3.6.2 - yup: 0.32.11 + yup: 1.0.2 zustand: 4.1.1_react@18.1.0 devDependencies: - '@babel/preset-env': 7.18.2_@babel+core@7.18.2 - '@babel/preset-react': 7.17.12_@babel+core@7.18.2 - '@babel/preset-typescript': 7.17.12_@babel+core@7.18.2 - '@jest/types': 29.0.3 - '@openapitools/openapi-generator-cli': 2.5.1 - '@swc/core': 1.3.22 - '@swc/jest': 0.2.24_@swc+core@1.3.22 - '@testing-library/dom': 8.13.0 - '@testing-library/jest-dom': 5.16.4 - '@testing-library/user-event': 14.4.3_tlwynutqiyp5mns3woioasuxnq + '@jest/types': 29.5.0 + '@openapitools/openapi-generator-cli': 2.5.2 + '@swc/core': 1.3.38 + '@swc/jest': 0.2.24_@swc+core@1.3.38 + '@testing-library/dom': 9.0.0 + '@testing-library/jest-dom': 5.16.5 + '@testing-library/user-event': 14.4.3_@testing-library+dom@9.0.0 '@types/eventsource': 1.1.8 - '@types/jest': 29.0.1 '@types/lodash': 4.14.177 '@types/node': 16.11.7 '@types/react': 18.0.9 - '@types/react-datepicker': 4.4.2_react@18.1.0 + '@types/react-datepicker': 4.10.0_react@18.1.0 '@types/react-dom': 18.0.5 '@types/react-router-dom': 5.3.3 '@types/styled-components': 5.1.18 @@ -167,41 +150,44 @@ devDependencies: eslint-config-airbnb: 19.0.4_iayhaebzx3saen2ll7sn5gqmdq eslint-config-airbnb-typescript: 17.0.0_l6wia5brkiej5f4nhesunbzj5y eslint-config-prettier: 8.5.0_eslint@8.16.0 - eslint-config-react-app: 7.0.1_qmzb7mn5nafzn657qxusdnr23a eslint-import-resolver-node: 0.3.6 eslint-import-resolver-typescript: 3.2.7_btspkuwbqkl4adpiufzbathtpi eslint-plugin-import: 2.26.0_h5azci6ujakbaa2xblg2jlxooy - eslint-plugin-jest-dom: 4.0.2_eslint@8.16.0 + eslint-plugin-jest-dom: 4.0.3_eslint@8.16.0 eslint-plugin-jsx-a11y: 6.5.1_eslint@8.16.0 - eslint-plugin-prettier: 4.0.0_q7a4ir2sdihdzpzdlnbgmzjlpq + eslint-plugin-prettier: 4.0.0_cu7djixscogveocpaizwew7lgy eslint-plugin-react: 7.30.1_eslint@8.16.0 eslint-plugin-react-hooks: 4.5.0_eslint@8.16.0 - husky: 8.0.1 - jest-environment-jsdom: 29.0.3 + jest-environment-jsdom: 29.5.0 jest-sonar-reporter: 2.0.0 - jest-styled-components: 7.0.8_styled-components@5.3.1 - lint-staged: 13.0.2 - prettier: 2.5.1 - rimraf: 3.0.2 - ts-node: 10.8.1_seagpw47opwyivxvtfydnuwcuy + jest-styled-components: 7.1.1_styled-components@5.3.1 + prettier: 2.8.4 + rimraf: 4.3.1 + ts-node: 10.9.1_p45lmeymo45ge573kyxcus632u ts-prune: 0.10.3 typescript: 4.7.4 vite-plugin-ejs: 1.6.4 packages: + /@adobe/css-tools/4.2.0: + resolution: {integrity: sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==} + dev: true + /@ampproject/remapping/2.2.0: resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} engines: {node: '>=6.0.0'} dependencies: '@jridgewell/gen-mapping': 0.1.1 '@jridgewell/trace-mapping': 0.3.15 + dev: false /@babel/code-frame/7.16.7: resolution: {integrity: sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==} engines: {node: '>=6.9.0'} dependencies: '@babel/highlight': 7.18.6 + dev: false /@babel/code-frame/7.18.6: resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} @@ -209,14 +195,10 @@ packages: dependencies: '@babel/highlight': 7.18.6 - /@babel/compat-data/7.17.10: - resolution: {integrity: sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==} - engines: {node: '>=6.9.0'} - dev: true - /@babel/compat-data/7.18.8: resolution: {integrity: sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==} engines: {node: '>=6.9.0'} + dev: false /@babel/core/7.18.2: resolution: {integrity: sha512-A8pri1YJiC5UnkdrWcmfZTJTV85b4UXTAfImGmCfYmax4TR9Cw8sDS0MOk++Gp2mE/BefVJ5nwy5yzqNJbP/DQ==} @@ -239,6 +221,7 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color + dev: false /@babel/core/7.18.9: resolution: {integrity: sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g==} @@ -261,20 +244,7 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color - - /@babel/eslint-parser/7.17.0_j4uj5cgi2mksbox6kqvi7jrs6u: - resolution: {integrity: sha512-PUEJ7ZBXbRkbq3qqM/jZ2nIuakUBqCYc7Qf52Lj7dlZ6zERnqisdHioL0l4wwQZnmskMeasqUNzLBFKs3nylXA==} - engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} - peerDependencies: - '@babel/core': '>=7.11.0' - eslint: ^7.5.0 || ^8.0.0 - dependencies: - '@babel/core': 7.18.2 - eslint: 8.16.0 - eslint-scope: 5.1.1 - eslint-visitor-keys: 2.1.0 - semver: 6.3.0 - dev: true + dev: false /@babel/generator/7.18.2: resolution: {integrity: sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==} @@ -283,6 +253,7 @@ packages: '@babel/types': 7.18.9 '@jridgewell/gen-mapping': 0.3.2 jsesc: 2.5.2 + dev: false /@babel/generator/7.18.9: resolution: {integrity: sha512-wt5Naw6lJrL1/SGkipMiFxJjtyczUWTP38deiP1PO60HsBjDeKk08CGC3S8iVuvf0FmTdgKwU1KIXzSKL1G0Ug==} @@ -292,27 +263,12 @@ packages: '@jridgewell/gen-mapping': 0.3.2 jsesc: 2.5.2 - /@babel/helper-annotate-as-pure/7.16.7: - resolution: {integrity: sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.18.9 - dev: true - /@babel/helper-annotate-as-pure/7.18.6: resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.18.9 - /@babel/helper-builder-binary-assignment-operator-visitor/7.16.7: - resolution: {integrity: sha512-C6FdbRaxYjwVu/geKW4ZeQ0Q31AftgRcdSnZ5/jsH6BzCJbtvXvhpfkbkThYSuutZA7nCXpPR6AD9zd1dprMkA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-explode-assignable-expression': 7.16.7 - '@babel/types': 7.18.9 - dev: true - /@babel/helper-compilation-targets/7.18.2_@babel+core@7.18.2: resolution: {integrity: sha512-s1jnPotJS9uQnzFtiZVBUxe67CuBa679oWFHpxYYnTpRL/1ffhyX44R9uYiXoa/pLXcY9H2moJta0iaanlk/rQ==} engines: {node: '>=6.9.0'} @@ -324,32 +280,7 @@ packages: '@babel/helper-validator-option': 7.18.6 browserslist: 4.20.4 semver: 6.3.0 - - /@babel/helper-compilation-targets/7.18.2_@babel+core@7.18.9: - resolution: {integrity: sha512-s1jnPotJS9uQnzFtiZVBUxe67CuBa679oWFHpxYYnTpRL/1ffhyX44R9uYiXoa/pLXcY9H2moJta0iaanlk/rQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.18.8 - '@babel/core': 7.18.9 - '@babel/helper-validator-option': 7.18.6 - browserslist: 4.20.4 - semver: 6.3.0 - dev: true - - /@babel/helper-compilation-targets/7.18.9_@babel+core@7.18.2: - resolution: {integrity: sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.18.8 - '@babel/core': 7.18.2 - '@babel/helper-validator-option': 7.18.6 - browserslist: 4.20.4 - semver: 6.3.0 - dev: true + dev: false /@babel/helper-compilation-targets/7.18.9_@babel+core@7.18.9: resolution: {integrity: sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==} @@ -362,112 +293,12 @@ packages: '@babel/helper-validator-option': 7.18.6 browserslist: 4.20.4 semver: 6.3.0 - - /@babel/helper-create-class-features-plugin/7.18.0_@babel+core@7.18.2: - resolution: {integrity: sha512-Kh8zTGR9de3J63e5nS0rQUdRs/kbtwoeQQ0sriS0lItjC96u8XXZN6lKpuyWd2coKSU13py/y+LTmThLuVX0Pg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.18.9 - '@babel/helper-member-expression-to-functions': 7.17.7 - '@babel/helper-optimise-call-expression': 7.16.7 - '@babel/helper-replace-supers': 7.18.2 - '@babel/helper-split-export-declaration': 7.18.6 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/helper-create-class-features-plugin/7.18.0_@babel+core@7.18.9: - resolution: {integrity: sha512-Kh8zTGR9de3J63e5nS0rQUdRs/kbtwoeQQ0sriS0lItjC96u8XXZN6lKpuyWd2coKSU13py/y+LTmThLuVX0Pg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.18.9 - '@babel/helper-member-expression-to-functions': 7.17.7 - '@babel/helper-optimise-call-expression': 7.16.7 - '@babel/helper-replace-supers': 7.18.2 - '@babel/helper-split-export-declaration': 7.18.6 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/helper-create-regexp-features-plugin/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-b2aZrV4zvutr9AIa6/gA3wsZKRwTKYoDxYiFKcESS3Ug2GTXzwBEvMuuFLhCQpEnRXs1zng4ISAXSUxxKBIcxw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-annotate-as-pure': 7.18.6 - regexpu-core: 5.0.1 - dev: true - - /@babel/helper-create-regexp-features-plugin/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-b2aZrV4zvutr9AIa6/gA3wsZKRwTKYoDxYiFKcESS3Ug2GTXzwBEvMuuFLhCQpEnRXs1zng4ISAXSUxxKBIcxw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-annotate-as-pure': 7.18.6 - regexpu-core: 5.0.1 - dev: true - - /@babel/helper-define-polyfill-provider/0.3.1_@babel+core@7.18.2: - resolution: {integrity: sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA==} - peerDependencies: - '@babel/core': ^7.4.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-compilation-targets': 7.18.9_@babel+core@7.18.2 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/traverse': 7.18.9 - debug: 4.3.4 - lodash.debounce: 4.0.8 - resolve: 1.22.1 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/helper-define-polyfill-provider/0.3.1_@babel+core@7.18.9: - resolution: {integrity: sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA==} - peerDependencies: - '@babel/core': ^7.4.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-compilation-targets': 7.18.9_@babel+core@7.18.9 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/traverse': 7.18.9 - debug: 4.3.4 - lodash.debounce: 4.0.8 - resolve: 1.22.1 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true + dev: false /@babel/helper-environment-visitor/7.18.9: resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} engines: {node: '>=6.9.0'} - /@babel/helper-explode-assignable-expression/7.16.7: - resolution: {integrity: sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.18.9 - dev: true - /@babel/helper-function-name/7.18.9: resolution: {integrity: sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==} engines: {node: '>=6.9.0'} @@ -481,13 +312,6 @@ packages: dependencies: '@babel/types': 7.18.9 - /@babel/helper-member-expression-to-functions/7.17.7: - resolution: {integrity: sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.18.9 - dev: true - /@babel/helper-module-imports/7.16.7: resolution: {integrity: sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==} engines: {node: '>=6.9.0'} @@ -514,6 +338,7 @@ packages: '@babel/types': 7.18.9 transitivePeerDependencies: - supports-color + dev: false /@babel/helper-module-transforms/7.18.9: resolution: {integrity: sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==} @@ -529,59 +354,19 @@ packages: '@babel/types': 7.18.9 transitivePeerDependencies: - supports-color - - /@babel/helper-optimise-call-expression/7.16.7: - resolution: {integrity: sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.18.9 - dev: true - - /@babel/helper-plugin-utils/7.17.12: - resolution: {integrity: sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA==} - engines: {node: '>=6.9.0'} - dev: true + dev: false /@babel/helper-plugin-utils/7.18.6: resolution: {integrity: sha512-gvZnm1YAAxh13eJdkb9EWHBnF3eAub3XTLCZEehHT2kWxiKVRL64+ae5Y6Ivne0mVHmMYKT+xWgZO+gQhuLUBg==} engines: {node: '>=6.9.0'} - - /@babel/helper-remap-async-to-generator/7.16.8: - resolution: {integrity: sha512-fm0gH7Flb8H51LqJHy3HJ3wnE1+qtYR2A99K06ahwrawLdOFsCEWjZOrYricXJHoPSudNKxrMBUPEIPxiIIvBw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-wrap-function': 7.16.8 - '@babel/types': 7.18.9 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/helper-replace-supers/7.18.2: - resolution: {integrity: sha512-XzAIyxx+vFnrOxiQrToSUOzUOn0e1J2Li40ntddek1Y69AXUTXoDJ40/D5RdjFu7s7qHiaeoTiempZcbuVXh2Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-member-expression-to-functions': 7.17.7 - '@babel/helper-optimise-call-expression': 7.16.7 - '@babel/traverse': 7.18.9 - '@babel/types': 7.18.9 - transitivePeerDependencies: - - supports-color - dev: true + dev: false /@babel/helper-simple-access/7.18.6: resolution: {integrity: sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.18.9 - - /@babel/helper-skip-transparent-expression-wrappers/7.16.0: - resolution: {integrity: sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.18.9 - dev: true + dev: false /@babel/helper-split-export-declaration/7.18.6: resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} @@ -593,26 +378,10 @@ packages: resolution: {integrity: sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-option/7.16.7: - resolution: {integrity: sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==} - engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-validator-option/7.18.6: resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} engines: {node: '>=6.9.0'} - - /@babel/helper-wrap-function/7.16.8: - resolution: {integrity: sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-function-name': 7.18.9 - '@babel/template': 7.18.6 - '@babel/traverse': 7.18.9 - '@babel/types': 7.18.9 - transitivePeerDependencies: - - supports-color - dev: true + dev: false /@babel/helpers/7.18.2: resolution: {integrity: sha512-j+d+u5xT5utcQSzrh9p+PaJX94h++KN+ng9b9WEJq7pkUPAd61FGqhjuUEdfknb3E/uDBb7ruwEeKkIxNJPIrg==} @@ -623,6 +392,7 @@ packages: '@babel/types': 7.18.9 transitivePeerDependencies: - supports-color + dev: false /@babel/helpers/7.18.9: resolution: {integrity: sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==} @@ -633,6 +403,7 @@ packages: '@babel/types': 7.18.9 transitivePeerDependencies: - supports-color + dev: false /@babel/highlight/7.18.6: resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} @@ -648,6 +419,7 @@ packages: hasBin: true dependencies: '@babel/types': 7.18.9 + dev: false /@babel/parser/7.18.9: resolution: {integrity: sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==} @@ -656,441 +428,6 @@ packages: dependencies: '@babel/types': 7.18.9 - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-xCJQXl4EeQ3J9C4yOmpTrtVGmzpm2iSzyxbkZHw7UCnZBftHpF/hpII80uWVyVrc40ytIClHjgWGTG1g/yB+aw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-xCJQXl4EeQ3J9C4yOmpTrtVGmzpm2iSzyxbkZHw7UCnZBftHpF/hpII80uWVyVrc40ytIClHjgWGTG1g/yB+aw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-/vt0hpIw0x4b6BLKUkwlvEoiGZYYLNZ96CzyHYPbtG2jZGz6LBe7/V+drYrc/d+ovrF9NBi0pmtvmNb/FsWtRQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.13.0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.16.0 - '@babel/plugin-proposal-optional-chaining': 7.17.12_@babel+core@7.18.2 - dev: true - - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-/vt0hpIw0x4b6BLKUkwlvEoiGZYYLNZ96CzyHYPbtG2jZGz6LBe7/V+drYrc/d+ovrF9NBi0pmtvmNb/FsWtRQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.13.0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.16.0 - '@babel/plugin-proposal-optional-chaining': 7.17.12_@babel+core@7.18.9 - dev: true - - /@babel/plugin-proposal-async-generator-functions/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-RWVvqD1ooLKP6IqWTA5GyFVX2isGEgC5iFxKzfYOIy/QEFdxYyCybBDtIGjipHpb9bDWHzcqGqFakf+mVmBTdQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-remap-async-to-generator': 7.16.8 - '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.18.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-async-generator-functions/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-RWVvqD1ooLKP6IqWTA5GyFVX2isGEgC5iFxKzfYOIy/QEFdxYyCybBDtIGjipHpb9bDWHzcqGqFakf+mVmBTdQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-remap-async-to-generator': 7.16.8 - '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.18.9 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-class-properties/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-U0mI9q8pW5Q9EaTHFPwSVusPMV/DV9Mm8p7csqROFLtIE9rBF5piLqyrBGigftALrBcsBGu4m38JneAe7ZDLXw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-create-class-features-plugin': 7.18.0_@babel+core@7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-class-properties/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-U0mI9q8pW5Q9EaTHFPwSVusPMV/DV9Mm8p7csqROFLtIE9rBF5piLqyrBGigftALrBcsBGu4m38JneAe7ZDLXw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-create-class-features-plugin': 7.18.0_@babel+core@7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-class-static-block/7.18.0_@babel+core@7.18.2: - resolution: {integrity: sha512-t+8LsRMMDE74c6sV7KShIw13sqbqd58tlqNrsWoWBTIMw7SVQ0cZ905wLNS/FBCy/3PyooRHLFFlfrUNyyz5lA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.12.0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-create-class-features-plugin': 7.18.0_@babel+core@7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.18.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-class-static-block/7.18.0_@babel+core@7.18.9: - resolution: {integrity: sha512-t+8LsRMMDE74c6sV7KShIw13sqbqd58tlqNrsWoWBTIMw7SVQ0cZ905wLNS/FBCy/3PyooRHLFFlfrUNyyz5lA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.12.0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-create-class-features-plugin': 7.18.0_@babel+core@7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.18.9 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-decorators/7.17.9_@babel+core@7.18.9: - resolution: {integrity: sha512-EfH2LZ/vPa2wuPwJ26j+kYRkaubf89UlwxKXtxqEm57HrgSEYDB8t4swFP+p8LcI9yiP9ZRJJjo/58hS6BnaDA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-create-class-features-plugin': 7.18.0_@babel+core@7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-replace-supers': 7.18.2 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/plugin-syntax-decorators': 7.17.0_@babel+core@7.18.9 - charcodes: 0.2.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-dynamic-import/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.18.2 - dev: true - - /@babel/plugin-proposal-dynamic-import/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.18.9 - dev: true - - /@babel/plugin-proposal-export-namespace-from/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-j7Ye5EWdwoXOpRmo5QmRyHPsDIe6+u70ZYZrd7uz+ebPYFKfRcLcNu3Ro0vOlJ5zuv8rU7xa+GttNiRzX56snQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.18.2 - dev: true - - /@babel/plugin-proposal-export-namespace-from/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-j7Ye5EWdwoXOpRmo5QmRyHPsDIe6+u70ZYZrd7uz+ebPYFKfRcLcNu3Ro0vOlJ5zuv8rU7xa+GttNiRzX56snQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.18.9 - dev: true - - /@babel/plugin-proposal-json-strings/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-rKJ+rKBoXwLnIn7n6o6fulViHMrOThz99ybH+hKHcOZbnN14VuMnH9fo2eHE69C8pO4uX1Q7t2HYYIDmv8VYkg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.18.2 - dev: true - - /@babel/plugin-proposal-json-strings/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-rKJ+rKBoXwLnIn7n6o6fulViHMrOThz99ybH+hKHcOZbnN14VuMnH9fo2eHE69C8pO4uX1Q7t2HYYIDmv8VYkg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.18.9 - dev: true - - /@babel/plugin-proposal-logical-assignment-operators/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-EqFo2s1Z5yy+JeJu7SFfbIUtToJTVlC61/C7WLKDntSw4Sz6JNAIfL7zQ74VvirxpjB5kz/kIx0gCcb+5OEo2Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.18.2 - dev: true - - /@babel/plugin-proposal-logical-assignment-operators/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-EqFo2s1Z5yy+JeJu7SFfbIUtToJTVlC61/C7WLKDntSw4Sz6JNAIfL7zQ74VvirxpjB5kz/kIx0gCcb+5OEo2Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.18.9 - dev: true - - /@babel/plugin-proposal-nullish-coalescing-operator/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-ws/g3FSGVzv+VH86+QvgtuJL/kR67xaEIF2x0iPqdDfYW6ra6JF3lKVBkWynRLcNtIC1oCTfDRVxmm2mKzy+ag==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.18.2 - dev: true - - /@babel/plugin-proposal-nullish-coalescing-operator/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-ws/g3FSGVzv+VH86+QvgtuJL/kR67xaEIF2x0iPqdDfYW6ra6JF3lKVBkWynRLcNtIC1oCTfDRVxmm2mKzy+ag==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.18.9 - dev: true - - /@babel/plugin-proposal-numeric-separator/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.18.2 - dev: true - - /@babel/plugin-proposal-numeric-separator/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.18.9 - dev: true - - /@babel/plugin-proposal-object-rest-spread/7.18.0_@babel+core@7.18.2: - resolution: {integrity: sha512-nbTv371eTrFabDfHLElkn9oyf9VG+VKK6WMzhY2o4eHKaG19BToD9947zzGMO6I/Irstx9d8CwX6njPNIAR/yw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.18.8 - '@babel/core': 7.18.2 - '@babel/helper-compilation-targets': 7.18.9_@babel+core@7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.18.2 - '@babel/plugin-transform-parameters': 7.17.12_@babel+core@7.18.2 - dev: true - - /@babel/plugin-proposal-object-rest-spread/7.18.0_@babel+core@7.18.9: - resolution: {integrity: sha512-nbTv371eTrFabDfHLElkn9oyf9VG+VKK6WMzhY2o4eHKaG19BToD9947zzGMO6I/Irstx9d8CwX6njPNIAR/yw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.18.8 - '@babel/core': 7.18.9 - '@babel/helper-compilation-targets': 7.18.9_@babel+core@7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.18.9 - '@babel/plugin-transform-parameters': 7.17.12_@babel+core@7.18.9 - dev: true - - /@babel/plugin-proposal-optional-catch-binding/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.18.2 - dev: true - - /@babel/plugin-proposal-optional-catch-binding/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.18.9 - dev: true - - /@babel/plugin-proposal-optional-chaining/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-7wigcOs/Z4YWlK7xxjkvaIw84vGhDv/P1dFGQap0nHkc8gFKY/r+hXc8Qzf5k1gY7CvGIcHqAnOagVKJJ1wVOQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.16.0 - '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.18.2 - dev: true - - /@babel/plugin-proposal-optional-chaining/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-7wigcOs/Z4YWlK7xxjkvaIw84vGhDv/P1dFGQap0nHkc8gFKY/r+hXc8Qzf5k1gY7CvGIcHqAnOagVKJJ1wVOQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.16.0 - '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.18.9 - dev: true - - /@babel/plugin-proposal-private-methods/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-SllXoxo19HmxhDWm3luPz+cPhtoTSKLJE9PXshsfrOzBqs60QP0r8OaJItrPhAj0d7mZMnNF0Y1UUggCDgMz1A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-create-class-features-plugin': 7.18.0_@babel+core@7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-private-methods/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-SllXoxo19HmxhDWm3luPz+cPhtoTSKLJE9PXshsfrOzBqs60QP0r8OaJItrPhAj0d7mZMnNF0Y1UUggCDgMz1A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-create-class-features-plugin': 7.18.0_@babel+core@7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-private-property-in-object/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-/6BtVi57CJfrtDNKfK5b66ydK2J5pXUKBKSPD2G1whamMuEnZWgoOIfO8Vf9F/DoD4izBLD/Au4NMQfruzzykg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-annotate-as-pure': 7.16.7 - '@babel/helper-create-class-features-plugin': 7.18.0_@babel+core@7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.18.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-private-property-in-object/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-/6BtVi57CJfrtDNKfK5b66ydK2J5pXUKBKSPD2G1whamMuEnZWgoOIfO8Vf9F/DoD4izBLD/Au4NMQfruzzykg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-annotate-as-pure': 7.16.7 - '@babel/helper-create-class-features-plugin': 7.18.0_@babel+core@7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.18.9 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-unicode-property-regex/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-Wb9qLjXf3ZazqXA7IvI7ozqRIXIGPtSo+L5coFmEkhTQK18ao4UDDD0zdTGAarmbLj2urpRwrc6893cu5Bfh0A==} - engines: {node: '>=4'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-create-regexp-features-plugin': 7.17.12_@babel+core@7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-proposal-unicode-property-regex/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-Wb9qLjXf3ZazqXA7IvI7ozqRIXIGPtSo+L5coFmEkhTQK18ao4UDDD0zdTGAarmbLj2urpRwrc6893cu5Bfh0A==} - engines: {node: '>=4'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-create-regexp-features-plugin': 7.17.12_@babel+core@7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-syntax-async-generators/7.8.4_@babel+core@7.18.2: - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - /@babel/plugin-syntax-async-generators/7.8.4_@babel+core@7.18.9: resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: @@ -1098,14 +435,6 @@ packages: dependencies: '@babel/core': 7.18.9 '@babel/helper-plugin-utils': 7.18.6 - - /@babel/plugin-syntax-bigint/7.8.3_@babel+core@7.18.2: - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 dev: false /@babel/plugin-syntax-bigint/7.8.3_@babel+core@7.18.9: @@ -1115,14 +444,7 @@ packages: dependencies: '@babel/core': 7.18.9 '@babel/helper-plugin-utils': 7.18.6 - - /@babel/plugin-syntax-class-properties/7.12.13_@babel+core@7.18.2: - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 + dev: false /@babel/plugin-syntax-class-properties/7.12.13_@babel+core@7.18.9: resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} @@ -1131,119 +453,6 @@ packages: dependencies: '@babel/core': 7.18.9 '@babel/helper-plugin-utils': 7.18.6 - - /@babel/plugin-syntax-class-static-block/7.14.5_@babel+core@7.18.2: - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-syntax-class-static-block/7.14.5_@babel+core@7.18.9: - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-syntax-decorators/7.17.0_@babel+core@7.18.9: - resolution: {integrity: sha512-qWe85yCXsvDEluNP0OyeQjH63DlhAR3W7K9BxxU1MvbDb48tgBG+Ao6IJJ6smPDrrVzSQZrbF6donpkFBMcs3A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-syntax-dynamic-import/7.8.3_@babel+core@7.18.2: - resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-syntax-dynamic-import/7.8.3_@babel+core@7.18.9: - resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-syntax-export-namespace-from/7.8.3_@babel+core@7.18.2: - resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-syntax-export-namespace-from/7.8.3_@babel+core@7.18.9: - resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-syntax-flow/7.18.6_@babel+core@7.18.2: - resolution: {integrity: sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - - /@babel/plugin-syntax-flow/7.18.6_@babel+core@7.18.9: - resolution: {integrity: sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-syntax-import-assertions/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-n/loy2zkq9ZEM8tEOwON9wTQSTNDTDEz6NujPtJGLU7qObzT1N4c4YZZf8E6ATB2AjNQg/Ib2AIpO03EZaCehw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-syntax-import-assertions/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-n/loy2zkq9ZEM8tEOwON9wTQSTNDTDEz6NujPtJGLU7qObzT1N4c4YZZf8E6ATB2AjNQg/Ib2AIpO03EZaCehw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-syntax-import-meta/7.10.4_@babel+core@7.18.2: - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 dev: false /@babel/plugin-syntax-import-meta/7.10.4_@babel+core@7.18.9: @@ -1253,14 +462,7 @@ packages: dependencies: '@babel/core': 7.18.9 '@babel/helper-plugin-utils': 7.18.6 - - /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.18.2: - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 + dev: false /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.18.9: resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} @@ -1269,15 +471,7 @@ packages: dependencies: '@babel/core': 7.18.9 '@babel/helper-plugin-utils': 7.18.6 - - /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.18.2: - resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 + dev: false /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.18.9: resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} @@ -1287,14 +481,7 @@ packages: dependencies: '@babel/core': 7.18.9 '@babel/helper-plugin-utils': 7.18.6 - - /@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.18.2: - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 + dev: false /@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.18.9: resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} @@ -1303,14 +490,7 @@ packages: dependencies: '@babel/core': 7.18.9 '@babel/helper-plugin-utils': 7.18.6 - - /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.18.2: - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 + dev: false /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.18.9: resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} @@ -1319,14 +499,7 @@ packages: dependencies: '@babel/core': 7.18.9 '@babel/helper-plugin-utils': 7.18.6 - - /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.18.2: - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 + dev: false /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.18.9: resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} @@ -1335,14 +508,7 @@ packages: dependencies: '@babel/core': 7.18.9 '@babel/helper-plugin-utils': 7.18.6 - - /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.18.2: - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 + dev: false /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.18.9: resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} @@ -1351,14 +517,7 @@ packages: dependencies: '@babel/core': 7.18.9 '@babel/helper-plugin-utils': 7.18.6 - - /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.18.2: - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 + dev: false /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.18.9: resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} @@ -1367,14 +526,7 @@ packages: dependencies: '@babel/core': 7.18.9 '@babel/helper-plugin-utils': 7.18.6 - - /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.18.2: - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 + dev: false /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.18.9: resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} @@ -1383,35 +535,7 @@ packages: dependencies: '@babel/core': 7.18.9 '@babel/helper-plugin-utils': 7.18.6 - - /@babel/plugin-syntax-private-property-in-object/7.14.5_@babel+core@7.18.2: - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-syntax-private-property-in-object/7.14.5_@babel+core@7.18.9: - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.18.2: - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 + dev: false /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.18.9: resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} @@ -1421,16 +545,7 @@ packages: dependencies: '@babel/core': 7.18.9 '@babel/helper-plugin-utils': 7.18.6 - - /@babel/plugin-syntax-typescript/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-TYY0SXFiO31YXtNg3HtFwNJHjLsAyIIhAhNWkQ5whPPS7HWUFlg9z0Ta4qAQNjQbP1wsSt/oKkmZ/4/WWdMUpw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true + dev: false /@babel/plugin-syntax-typescript/7.17.12_@babel+core@7.18.9: resolution: {integrity: sha512-TYY0SXFiO31YXtNg3HtFwNJHjLsAyIIhAhNWkQ5whPPS7HWUFlg9z0Ta4qAQNjQbP1wsSt/oKkmZ/4/WWdMUpw==} @@ -1440,1131 +555,7 @@ packages: dependencies: '@babel/core': 7.18.9 '@babel/helper-plugin-utils': 7.18.6 - - /@babel/plugin-transform-arrow-functions/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-PHln3CNi/49V+mza4xMwrg+WGYevSF1oaiXaC2EQfdp4HWlSjRsrDXWJiQBKpP7749u6vQ9mcry2uuFOv5CXvA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-arrow-functions/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-PHln3CNi/49V+mza4xMwrg+WGYevSF1oaiXaC2EQfdp4HWlSjRsrDXWJiQBKpP7749u6vQ9mcry2uuFOv5CXvA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-async-to-generator/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-J8dbrWIOO3orDzir57NRsjg4uxucvhby0L/KZuGsWDj0g7twWK3g7JhJhOrXtuXiw8MeiSdJ3E0OW9H8LYEzLQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-remap-async-to-generator': 7.16.8 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-async-to-generator/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-J8dbrWIOO3orDzir57NRsjg4uxucvhby0L/KZuGsWDj0g7twWK3g7JhJhOrXtuXiw8MeiSdJ3E0OW9H8LYEzLQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-remap-async-to-generator': 7.16.8 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-block-scoped-functions/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-block-scoped-functions/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-block-scoping/7.18.4_@babel+core@7.18.2: - resolution: {integrity: sha512-+Hq10ye+jlvLEogSOtq4mKvtk7qwcUQ1f0Mrueai866C82f844Yom2cttfJdMdqRLTxWpsbfbkIkOIfovyUQXw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-block-scoping/7.18.4_@babel+core@7.18.9: - resolution: {integrity: sha512-+Hq10ye+jlvLEogSOtq4mKvtk7qwcUQ1f0Mrueai866C82f844Yom2cttfJdMdqRLTxWpsbfbkIkOIfovyUQXw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-classes/7.18.4_@babel+core@7.18.2: - resolution: {integrity: sha512-e42NSG2mlKWgxKUAD9EJJSkZxR67+wZqzNxLSpc51T8tRU5SLFHsPmgYR5yr7sdgX4u+iHA1C5VafJ6AyImV3A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-annotate-as-pure': 7.16.7 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.18.9 - '@babel/helper-optimise-call-expression': 7.16.7 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-replace-supers': 7.18.2 - '@babel/helper-split-export-declaration': 7.18.6 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-classes/7.18.4_@babel+core@7.18.9: - resolution: {integrity: sha512-e42NSG2mlKWgxKUAD9EJJSkZxR67+wZqzNxLSpc51T8tRU5SLFHsPmgYR5yr7sdgX4u+iHA1C5VafJ6AyImV3A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-annotate-as-pure': 7.16.7 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.18.9 - '@babel/helper-optimise-call-expression': 7.16.7 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-replace-supers': 7.18.2 - '@babel/helper-split-export-declaration': 7.18.6 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-computed-properties/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-a7XINeplB5cQUWMg1E/GI1tFz3LfK021IjV1rj1ypE+R7jHm+pIHmHl25VNkZxtx9uuYp7ThGk8fur1HHG7PgQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-computed-properties/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-a7XINeplB5cQUWMg1E/GI1tFz3LfK021IjV1rj1ypE+R7jHm+pIHmHl25VNkZxtx9uuYp7ThGk8fur1HHG7PgQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-destructuring/7.18.0_@babel+core@7.18.2: - resolution: {integrity: sha512-Mo69klS79z6KEfrLg/1WkmVnB8javh75HX4pi2btjvlIoasuxilEyjtsQW6XPrubNd7AQy0MMaNIaQE4e7+PQw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-destructuring/7.18.0_@babel+core@7.18.9: - resolution: {integrity: sha512-Mo69klS79z6KEfrLg/1WkmVnB8javh75HX4pi2btjvlIoasuxilEyjtsQW6XPrubNd7AQy0MMaNIaQE4e7+PQw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-dotall-regex/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-create-regexp-features-plugin': 7.17.12_@babel+core@7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-dotall-regex/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-create-regexp-features-plugin': 7.17.12_@babel+core@7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-duplicate-keys/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-EA5eYFUG6xeerdabina/xIoB95jJ17mAkR8ivx6ZSu9frKShBjpOGZPn511MTDTkiCO+zXnzNczvUM69YSf3Zw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-duplicate-keys/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-EA5eYFUG6xeerdabina/xIoB95jJ17mAkR8ivx6ZSu9frKShBjpOGZPn511MTDTkiCO+zXnzNczvUM69YSf3Zw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-exponentiation-operator/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.16.7 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-exponentiation-operator/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.16.7 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-flow-strip-types/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-mzmCq3cNsDpZZu9FADYYyfZJIOrSONmHcop2XEKPdBNMa4PDC4eEvcOvzZaCNcjKu72v0XQlA5y1g58aLRXdYg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-flow': 7.18.6_@babel+core@7.18.9 - dev: true - - /@babel/plugin-transform-for-of/7.18.1_@babel+core@7.18.2: - resolution: {integrity: sha512-+TTB5XwvJ5hZbO8xvl2H4XaMDOAK57zF4miuC9qQJgysPNEAZZ9Z69rdF5LJkozGdZrjBIUAIyKUWRMmebI7vg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-for-of/7.18.1_@babel+core@7.18.9: - resolution: {integrity: sha512-+TTB5XwvJ5hZbO8xvl2H4XaMDOAK57zF4miuC9qQJgysPNEAZZ9Z69rdF5LJkozGdZrjBIUAIyKUWRMmebI7vg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-function-name/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-compilation-targets': 7.18.9_@babel+core@7.18.2 - '@babel/helper-function-name': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-function-name/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-compilation-targets': 7.18.9_@babel+core@7.18.9 - '@babel/helper-function-name': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-literals/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-8iRkvaTjJciWycPIZ9k9duu663FT7VrBdNqNgxnVXEFwOIp55JWcZd23VBRySYbnS3PwQ3rGiabJBBBGj5APmQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-literals/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-8iRkvaTjJciWycPIZ9k9duu663FT7VrBdNqNgxnVXEFwOIp55JWcZd23VBRySYbnS3PwQ3rGiabJBBBGj5APmQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-member-expression-literals/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-member-expression-literals/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-modules-amd/7.18.0_@babel+core@7.18.2: - resolution: {integrity: sha512-h8FjOlYmdZwl7Xm2Ug4iX2j7Qy63NANI+NQVWQzv6r25fqgg7k2dZl03p95kvqNclglHs4FZ+isv4p1uXMA+QA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-module-transforms': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-modules-amd/7.18.0_@babel+core@7.18.9: - resolution: {integrity: sha512-h8FjOlYmdZwl7Xm2Ug4iX2j7Qy63NANI+NQVWQzv6r25fqgg7k2dZl03p95kvqNclglHs4FZ+isv4p1uXMA+QA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-module-transforms': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-modules-commonjs/7.18.6_@babel+core@7.18.2: - resolution: {integrity: sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-module-transforms': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-simple-access': 7.18.6 - babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-modules-commonjs/7.18.6_@babel+core@7.18.9: - resolution: {integrity: sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-module-transforms': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-simple-access': 7.18.6 - babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-modules-systemjs/7.18.4_@babel+core@7.18.2: - resolution: {integrity: sha512-lH2UaQaHVOAeYrUUuZ8i38o76J/FnO8vu21OE+tD1MyP9lxdZoSfz+pDbWkq46GogUrdrMz3tiz/FYGB+bVThg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-module-transforms': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-validator-identifier': 7.18.6 - babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-modules-systemjs/7.18.4_@babel+core@7.18.9: - resolution: {integrity: sha512-lH2UaQaHVOAeYrUUuZ8i38o76J/FnO8vu21OE+tD1MyP9lxdZoSfz+pDbWkq46GogUrdrMz3tiz/FYGB+bVThg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-module-transforms': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-validator-identifier': 7.18.6 - babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-modules-umd/7.18.0_@babel+core@7.18.2: - resolution: {integrity: sha512-d/zZ8I3BWli1tmROLxXLc9A6YXvGK8egMxHp+E/rRwMh1Kip0AP77VwZae3snEJ33iiWwvNv2+UIIhfalqhzZA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-module-transforms': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-modules-umd/7.18.0_@babel+core@7.18.9: - resolution: {integrity: sha512-d/zZ8I3BWli1tmROLxXLc9A6YXvGK8egMxHp+E/rRwMh1Kip0AP77VwZae3snEJ33iiWwvNv2+UIIhfalqhzZA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-module-transforms': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-named-capturing-groups-regex/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-vWoWFM5CKaTeHrdUJ/3SIOTRV+MBVGybOC9mhJkaprGNt5demMymDW24yC74avb915/mIRe3TgNb/d8idvnCRA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-create-regexp-features-plugin': 7.17.12_@babel+core@7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-named-capturing-groups-regex/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-vWoWFM5CKaTeHrdUJ/3SIOTRV+MBVGybOC9mhJkaprGNt5demMymDW24yC74avb915/mIRe3TgNb/d8idvnCRA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-create-regexp-features-plugin': 7.17.12_@babel+core@7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-new-target/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-CaOtzk2fDYisbjAD4Sd1MTKGVIpRtx9bWLyj24Y/k6p4s4gQ3CqDGJauFJxt8M/LEx003d0i3klVqnN73qvK3w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-new-target/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-CaOtzk2fDYisbjAD4Sd1MTKGVIpRtx9bWLyj24Y/k6p4s4gQ3CqDGJauFJxt8M/LEx003d0i3klVqnN73qvK3w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-object-super/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-replace-supers': 7.18.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-object-super/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-replace-supers': 7.18.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-parameters/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-6qW4rWo1cyCdq1FkYri7AHpauchbGLXpdwnYsfxFb+KtddHENfsY5JZb35xUwkK5opOLcJ3BNd2l7PhRYGlwIA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-parameters/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-6qW4rWo1cyCdq1FkYri7AHpauchbGLXpdwnYsfxFb+KtddHENfsY5JZb35xUwkK5opOLcJ3BNd2l7PhRYGlwIA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-property-literals/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-property-literals/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-react-display-name/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-qgIg8BcZgd0G/Cz916D5+9kqX0c7nPZyXaP8R2tLNN5tkyIZdG5fEwBrxwplzSnjC1jvQmyMNVwUCZPcbGY7Pg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-react-display-name/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-qgIg8BcZgd0G/Cz916D5+9kqX0c7nPZyXaP8R2tLNN5tkyIZdG5fEwBrxwplzSnjC1jvQmyMNVwUCZPcbGY7Pg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-react-jsx-development/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/plugin-transform-react-jsx': 7.18.6_@babel+core@7.18.2 - dev: true - - /@babel/plugin-transform-react-jsx-development/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/plugin-transform-react-jsx': 7.18.6_@babel+core@7.18.9 - dev: true - - /@babel/plugin-transform-react-jsx/7.18.6_@babel+core@7.18.2: - resolution: {integrity: sha512-Mz7xMPxoy9kPS/JScj6fJs03TZ/fZ1dJPlMjRAgTaxaS0fUBk8FV/A2rRgfPsVCZqALNwMexD+0Uaf5zlcKPpw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.18.2 - '@babel/types': 7.18.7 - - /@babel/plugin-transform-react-jsx/7.18.6_@babel+core@7.18.9: - resolution: {integrity: sha512-Mz7xMPxoy9kPS/JScj6fJs03TZ/fZ1dJPlMjRAgTaxaS0fUBk8FV/A2rRgfPsVCZqALNwMexD+0Uaf5zlcKPpw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.18.9 - '@babel/types': 7.18.7 - dev: true - - /@babel/plugin-transform-react-pure-annotations/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-hs71ToC97k3QWxswh2ElzMFABXHvGiJ01IB1TbYQDGeWRKWz/MPUTh5jGExdHvosYKpnJW5Pm3S4+TA3FyX+GA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-annotate-as-pure': 7.16.7 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-react-pure-annotations/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-hs71ToC97k3QWxswh2ElzMFABXHvGiJ01IB1TbYQDGeWRKWz/MPUTh5jGExdHvosYKpnJW5Pm3S4+TA3FyX+GA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-annotate-as-pure': 7.16.7 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-regenerator/7.18.0_@babel+core@7.18.2: - resolution: {integrity: sha512-C8YdRw9uzx25HSIzwA7EM7YP0FhCe5wNvJbZzjVNHHPGVcDJ3Aie+qGYYdS1oVQgn+B3eAIJbWFLrJ4Jipv7nw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - regenerator-transform: 0.15.0 - dev: true - - /@babel/plugin-transform-regenerator/7.18.0_@babel+core@7.18.9: - resolution: {integrity: sha512-C8YdRw9uzx25HSIzwA7EM7YP0FhCe5wNvJbZzjVNHHPGVcDJ3Aie+qGYYdS1oVQgn+B3eAIJbWFLrJ4Jipv7nw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - regenerator-transform: 0.15.0 - dev: true - - /@babel/plugin-transform-reserved-words/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-1KYqwbJV3Co03NIi14uEHW8P50Md6KqFgt0FfpHdK6oyAHQVTosgPuPSiWud1HX0oYJ1hGRRlk0fP87jFpqXZA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-reserved-words/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-1KYqwbJV3Co03NIi14uEHW8P50Md6KqFgt0FfpHdK6oyAHQVTosgPuPSiWud1HX0oYJ1hGRRlk0fP87jFpqXZA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-runtime/7.17.10_@babel+core@7.18.9: - resolution: {integrity: sha512-6jrMilUAJhktTr56kACL8LnWC5hx3Lf27BS0R0DSyW/OoJfb/iTHeE96V3b1dgKG3FSFdd/0culnYWMkjcKCig==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.18.6 - babel-plugin-polyfill-corejs2: 0.3.1_@babel+core@7.18.9 - babel-plugin-polyfill-corejs3: 0.5.2_@babel+core@7.18.9 - babel-plugin-polyfill-regenerator: 0.3.1_@babel+core@7.18.9 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-shorthand-properties/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-shorthand-properties/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-spread/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-9pgmuQAtFi3lpNUstvG9nGfk9DkrdmWNp9KeKPFmuZCpEnxRzYlS8JgwPjYj+1AWDOSvoGN0H30p1cBOmT/Svg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.16.0 - dev: true - - /@babel/plugin-transform-spread/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-9pgmuQAtFi3lpNUstvG9nGfk9DkrdmWNp9KeKPFmuZCpEnxRzYlS8JgwPjYj+1AWDOSvoGN0H30p1cBOmT/Svg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.16.0 - dev: true - - /@babel/plugin-transform-sticky-regex/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-sticky-regex/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-template-literals/7.18.2_@babel+core@7.18.2: - resolution: {integrity: sha512-/cmuBVw9sZBGZVOMkpAEaVLwm4JmK2GZ1dFKOGGpMzEHWFmyZZ59lUU0PdRr8YNYeQdNzTDwuxP2X2gzydTc9g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-template-literals/7.18.2_@babel+core@7.18.9: - resolution: {integrity: sha512-/cmuBVw9sZBGZVOMkpAEaVLwm4JmK2GZ1dFKOGGpMzEHWFmyZZ59lUU0PdRr8YNYeQdNzTDwuxP2X2gzydTc9g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-typeof-symbol/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-Q8y+Jp7ZdtSPXCThB6zjQ74N3lj0f6TDh1Hnf5B+sYlzQ8i5Pjp8gW0My79iekSpT4WnI06blqP6DT0OmaXXmw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-typeof-symbol/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-Q8y+Jp7ZdtSPXCThB6zjQ74N3lj0f6TDh1Hnf5B+sYlzQ8i5Pjp8gW0My79iekSpT4WnI06blqP6DT0OmaXXmw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-typescript/7.18.4_@babel+core@7.18.2: - resolution: {integrity: sha512-l4vHuSLUajptpHNEOUDEGsnpl9pfRLsN1XUoDQDD/YBuXTM+v37SHGS+c6n4jdcZy96QtuUuSvZYMLSSsjH8Mw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-create-class-features-plugin': 7.18.0_@babel+core@7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-typescript': 7.17.12_@babel+core@7.18.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-typescript/7.18.4_@babel+core@7.18.9: - resolution: {integrity: sha512-l4vHuSLUajptpHNEOUDEGsnpl9pfRLsN1XUoDQDD/YBuXTM+v37SHGS+c6n4jdcZy96QtuUuSvZYMLSSsjH8Mw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-create-class-features-plugin': 7.18.0_@babel+core@7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-syntax-typescript': 7.17.12_@babel+core@7.18.9 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-unicode-escapes/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-unicode-escapes/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-unicode-regex/7.16.7_@babel+core@7.18.2: - resolution: {integrity: sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-create-regexp-features-plugin': 7.17.12_@babel+core@7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/plugin-transform-unicode-regex/7.16.7_@babel+core@7.18.9: - resolution: {integrity: sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-create-regexp-features-plugin': 7.17.12_@babel+core@7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - dev: true - - /@babel/preset-env/7.18.2_@babel+core@7.18.2: - resolution: {integrity: sha512-PfpdxotV6afmXMU47S08F9ZKIm2bJIQ0YbAAtDfIENX7G1NUAXigLREh69CWDjtgUy7dYn7bsMzkgdtAlmS68Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.17.10 - '@babel/core': 7.18.2 - '@babel/helper-compilation-targets': 7.18.2_@babel+core@7.18.2 - '@babel/helper-plugin-utils': 7.17.12 - '@babel/helper-validator-option': 7.16.7 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-proposal-async-generator-functions': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-proposal-class-properties': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-proposal-class-static-block': 7.18.0_@babel+core@7.18.2 - '@babel/plugin-proposal-dynamic-import': 7.16.7_@babel+core@7.18.2 - '@babel/plugin-proposal-export-namespace-from': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-proposal-json-strings': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-proposal-logical-assignment-operators': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-proposal-nullish-coalescing-operator': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-proposal-numeric-separator': 7.16.7_@babel+core@7.18.2 - '@babel/plugin-proposal-object-rest-spread': 7.18.0_@babel+core@7.18.2 - '@babel/plugin-proposal-optional-catch-binding': 7.16.7_@babel+core@7.18.2 - '@babel/plugin-proposal-optional-chaining': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-proposal-private-methods': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-proposal-private-property-in-object': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-proposal-unicode-property-regex': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.18.2 - '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.18.2 - '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.18.2 - '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.18.2 - '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.18.2 - '@babel/plugin-syntax-import-assertions': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.18.2 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.18.2 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.18.2 - '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.18.2 - '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.18.2 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.18.2 - '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.18.2 - '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.18.2 - '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.18.2 - '@babel/plugin-transform-arrow-functions': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-transform-async-to-generator': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-transform-block-scoped-functions': 7.16.7_@babel+core@7.18.2 - '@babel/plugin-transform-block-scoping': 7.18.4_@babel+core@7.18.2 - '@babel/plugin-transform-classes': 7.18.4_@babel+core@7.18.2 - '@babel/plugin-transform-computed-properties': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-transform-destructuring': 7.18.0_@babel+core@7.18.2 - '@babel/plugin-transform-dotall-regex': 7.16.7_@babel+core@7.18.2 - '@babel/plugin-transform-duplicate-keys': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-transform-exponentiation-operator': 7.16.7_@babel+core@7.18.2 - '@babel/plugin-transform-for-of': 7.18.1_@babel+core@7.18.2 - '@babel/plugin-transform-function-name': 7.16.7_@babel+core@7.18.2 - '@babel/plugin-transform-literals': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-transform-member-expression-literals': 7.16.7_@babel+core@7.18.2 - '@babel/plugin-transform-modules-amd': 7.18.0_@babel+core@7.18.2 - '@babel/plugin-transform-modules-commonjs': 7.18.6_@babel+core@7.18.2 - '@babel/plugin-transform-modules-systemjs': 7.18.4_@babel+core@7.18.2 - '@babel/plugin-transform-modules-umd': 7.18.0_@babel+core@7.18.2 - '@babel/plugin-transform-named-capturing-groups-regex': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-transform-new-target': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-transform-object-super': 7.16.7_@babel+core@7.18.2 - '@babel/plugin-transform-parameters': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-transform-property-literals': 7.16.7_@babel+core@7.18.2 - '@babel/plugin-transform-regenerator': 7.18.0_@babel+core@7.18.2 - '@babel/plugin-transform-reserved-words': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-transform-shorthand-properties': 7.16.7_@babel+core@7.18.2 - '@babel/plugin-transform-spread': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-transform-sticky-regex': 7.16.7_@babel+core@7.18.2 - '@babel/plugin-transform-template-literals': 7.18.2_@babel+core@7.18.2 - '@babel/plugin-transform-typeof-symbol': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-transform-unicode-escapes': 7.16.7_@babel+core@7.18.2 - '@babel/plugin-transform-unicode-regex': 7.16.7_@babel+core@7.18.2 - '@babel/preset-modules': 0.1.5_@babel+core@7.18.2 - '@babel/types': 7.18.4 - babel-plugin-polyfill-corejs2: 0.3.1_@babel+core@7.18.2 - babel-plugin-polyfill-corejs3: 0.5.2_@babel+core@7.18.2 - babel-plugin-polyfill-regenerator: 0.3.1_@babel+core@7.18.2 - core-js-compat: 3.22.5 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/preset-env/7.18.2_@babel+core@7.18.9: - resolution: {integrity: sha512-PfpdxotV6afmXMU47S08F9ZKIm2bJIQ0YbAAtDfIENX7G1NUAXigLREh69CWDjtgUy7dYn7bsMzkgdtAlmS68Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.17.10 - '@babel/core': 7.18.9 - '@babel/helper-compilation-targets': 7.18.2_@babel+core@7.18.9 - '@babel/helper-plugin-utils': 7.17.12 - '@babel/helper-validator-option': 7.16.7 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-proposal-async-generator-functions': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-proposal-class-properties': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-proposal-class-static-block': 7.18.0_@babel+core@7.18.9 - '@babel/plugin-proposal-dynamic-import': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-proposal-export-namespace-from': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-proposal-json-strings': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-proposal-logical-assignment-operators': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-proposal-nullish-coalescing-operator': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-proposal-numeric-separator': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-proposal-object-rest-spread': 7.18.0_@babel+core@7.18.9 - '@babel/plugin-proposal-optional-catch-binding': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-proposal-optional-chaining': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-proposal-private-methods': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-proposal-private-property-in-object': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-proposal-unicode-property-regex': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.18.9 - '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.18.9 - '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.18.9 - '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.18.9 - '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.18.9 - '@babel/plugin-syntax-import-assertions': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.18.9 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.18.9 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.18.9 - '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.18.9 - '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.18.9 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.18.9 - '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.18.9 - '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.18.9 - '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.18.9 - '@babel/plugin-transform-arrow-functions': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-transform-async-to-generator': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-transform-block-scoped-functions': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-transform-block-scoping': 7.18.4_@babel+core@7.18.9 - '@babel/plugin-transform-classes': 7.18.4_@babel+core@7.18.9 - '@babel/plugin-transform-computed-properties': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-transform-destructuring': 7.18.0_@babel+core@7.18.9 - '@babel/plugin-transform-dotall-regex': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-transform-duplicate-keys': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-transform-exponentiation-operator': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-transform-for-of': 7.18.1_@babel+core@7.18.9 - '@babel/plugin-transform-function-name': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-transform-literals': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-transform-member-expression-literals': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-transform-modules-amd': 7.18.0_@babel+core@7.18.9 - '@babel/plugin-transform-modules-commonjs': 7.18.6_@babel+core@7.18.9 - '@babel/plugin-transform-modules-systemjs': 7.18.4_@babel+core@7.18.9 - '@babel/plugin-transform-modules-umd': 7.18.0_@babel+core@7.18.9 - '@babel/plugin-transform-named-capturing-groups-regex': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-transform-new-target': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-transform-object-super': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-transform-parameters': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-transform-property-literals': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-transform-regenerator': 7.18.0_@babel+core@7.18.9 - '@babel/plugin-transform-reserved-words': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-transform-shorthand-properties': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-transform-spread': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-transform-sticky-regex': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-transform-template-literals': 7.18.2_@babel+core@7.18.9 - '@babel/plugin-transform-typeof-symbol': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-transform-unicode-escapes': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-transform-unicode-regex': 7.16.7_@babel+core@7.18.9 - '@babel/preset-modules': 0.1.5_@babel+core@7.18.9 - '@babel/types': 7.18.4 - babel-plugin-polyfill-corejs2: 0.3.1_@babel+core@7.18.9 - babel-plugin-polyfill-corejs3: 0.5.2_@babel+core@7.18.9 - babel-plugin-polyfill-regenerator: 0.3.1_@babel+core@7.18.9 - core-js-compat: 3.22.5 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/preset-modules/0.1.5_@babel+core@7.18.2: - resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-proposal-unicode-property-regex': 7.17.12_@babel+core@7.18.2 - '@babel/plugin-transform-dotall-regex': 7.16.7_@babel+core@7.18.2 - '@babel/types': 7.18.9 - esutils: 2.0.3 - dev: true - - /@babel/preset-modules/0.1.5_@babel+core@7.18.9: - resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.18.6 - '@babel/plugin-proposal-unicode-property-regex': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-transform-dotall-regex': 7.16.7_@babel+core@7.18.9 - '@babel/types': 7.18.9 - esutils: 2.0.3 - dev: true - - /@babel/preset-react/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-h5U+rwreXtZaRBEQhW1hOJLMq8XNJBQ/9oymXiCXTuT/0uOwpbT0gUt+sXeOqoXBgNuUKI7TaObVwoEyWkpFgA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.17.12 - '@babel/helper-validator-option': 7.16.7 - '@babel/plugin-transform-react-display-name': 7.16.7_@babel+core@7.18.2 - '@babel/plugin-transform-react-jsx': 7.18.6_@babel+core@7.18.2 - '@babel/plugin-transform-react-jsx-development': 7.16.7_@babel+core@7.18.2 - '@babel/plugin-transform-react-pure-annotations': 7.16.7_@babel+core@7.18.2 - dev: true - - /@babel/preset-react/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-h5U+rwreXtZaRBEQhW1hOJLMq8XNJBQ/9oymXiCXTuT/0uOwpbT0gUt+sXeOqoXBgNuUKI7TaObVwoEyWkpFgA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.17.12 - '@babel/helper-validator-option': 7.16.7 - '@babel/plugin-transform-react-display-name': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-transform-react-jsx': 7.18.6_@babel+core@7.18.9 - '@babel/plugin-transform-react-jsx-development': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-transform-react-pure-annotations': 7.16.7_@babel+core@7.18.9 - dev: true - - /@babel/preset-typescript/7.17.12_@babel+core@7.18.2: - resolution: {integrity: sha512-S1ViF8W2QwAKUGJXxP9NAfNaqGDdEBJKpYkxHf5Yy2C4NPPzXGeR3Lhk7G8xJaaLcFTRfNjVbtbVtm8Gb0mqvg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-plugin-utils': 7.17.12 - '@babel/helper-validator-option': 7.16.7 - '@babel/plugin-transform-typescript': 7.18.4_@babel+core@7.18.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/preset-typescript/7.17.12_@babel+core@7.18.9: - resolution: {integrity: sha512-S1ViF8W2QwAKUGJXxP9NAfNaqGDdEBJKpYkxHf5Yy2C4NPPzXGeR3Lhk7G8xJaaLcFTRfNjVbtbVtm8Gb0mqvg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-plugin-utils': 7.17.12 - '@babel/helper-validator-option': 7.16.7 - '@babel/plugin-transform-typescript': 7.18.4_@babel+core@7.18.9 - transitivePeerDependencies: - - supports-color - dev: true + dev: false /@babel/runtime-corejs3/7.14.6: resolution: {integrity: sha512-Xl8SPYtdjcMoCsIM4teyVRg7jIcgl8F2kRtoCcXuHzXswt9UxZCS6BzRo8fcnCuP6u2XtPgvyonmEPF57Kxo9Q==} @@ -2587,6 +578,7 @@ packages: '@babel/code-frame': 7.18.6 '@babel/parser': 7.18.9 '@babel/types': 7.18.9 + dev: false /@babel/template/7.18.6: resolution: {integrity: sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw==} @@ -2612,6 +604,7 @@ packages: globals: 11.12.0 transitivePeerDependencies: - supports-color + dev: false /@babel/traverse/7.18.2_supports-color@5.5.0: resolution: {integrity: sha512-9eNwoeovJ6KH9zcCNnENY7DMFwTU9JdGCFtqNLfUAqtUHRCOsTOqWoffosP8vKmNYeSBUv3yVJXjfd8ucwOjUA==} @@ -2646,6 +639,7 @@ packages: globals: 11.12.0 transitivePeerDependencies: - supports-color + dev: false /@babel/types/7.18.4: resolution: {integrity: sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw==} @@ -2653,13 +647,7 @@ packages: dependencies: '@babel/helper-validator-identifier': 7.18.6 to-fast-properties: 2.0.0 - - /@babel/types/7.18.7: - resolution: {integrity: sha512-QG3yxTcTIBoAcQmkCs+wAPYZhu7Dk9rXKacINfNbdJDNERTbLQbHGyVG8q/YGMPeCJRIhSY0+fTc5+xuh6WPSQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.18.6 - to-fast-properties: 2.0.0 + dev: false /@babel/types/7.18.9: resolution: {integrity: sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==} @@ -2670,6 +658,7 @@ packages: /@bcoe/v8-coverage/0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: false /@cspotcode/source-map-support/0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -2884,42 +873,43 @@ packages: - supports-color dev: true - /@floating-ui/core/1.0.1: - resolution: {integrity: sha512-bO37brCPfteXQfFY0DyNDGB3+IMe4j150KFQcgJ5aBP295p9nBGeHEs/p0czrRbtlHq4Px/yoPXO/+dOCcF4uA==} + /@floating-ui/core/1.2.1: + resolution: {integrity: sha512-LSqwPZkK3rYfD7GKoIeExXOyYx6Q1O4iqZWwIehDNuv3Dv425FIAE8PRwtAx1imEolFTHgBEcoFHm9MDnYgPCg==} dev: false - /@floating-ui/dom/1.0.4: - resolution: {integrity: sha512-maYJRv+sAXTy4K9mzdv0JPyNW5YPVHrqtY90tEdI6XNpuLOP26Ci2pfwPsKBA/Wh4Z3FX5sUrtUFTdMYj9v+ug==} + /@floating-ui/dom/1.2.1: + resolution: {integrity: sha512-Rt45SmRiV8eU+xXSB9t0uMYiQ/ZWGE/jumse2o3i5RGlyvcbqOF4q+1qBnzLE2kZ5JGhq0iMkcGXUKbFe7MpTA==} dependencies: - '@floating-ui/core': 1.0.1 + '@floating-ui/core': 1.2.1 dev: false - /@floating-ui/react-dom-interactions/0.10.3_ohobp6rpsmerwlq5ipwfh5yigy: - resolution: {integrity: sha512-UEHqdnzyoiWNU5az/tAljr9iXFzN18DcvpMqW+/cXz4FEhDEB1ogLtWldOWCujLerPBnSRocADALafelOReMpw==} + /@floating-ui/react-dom/1.3.0_ef5jwxihqo6n7gxfmzogljlgcm: + resolution: {integrity: sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: - '@floating-ui/react-dom': 1.0.0_ef5jwxihqo6n7gxfmzogljlgcm + '@floating-ui/dom': 1.2.1 + react: 18.1.0 + react-dom: 18.1.0_react@18.1.0 + dev: false + + /@floating-ui/react/0.19.2_ohobp6rpsmerwlq5ipwfh5yigy: + resolution: {integrity: sha512-JyNk4A0Ezirq8FlXECvRtQOX/iBe5Ize0W/pLkrZjfHW9GUV7Xnq6zm6fyZuQzaHHqEnVizmvlA96e1/CkZv+w==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/react-dom': 1.3.0_ef5jwxihqo6n7gxfmzogljlgcm aria-hidden: 1.2.1_7cpxmzzodpxnolj5zcc5cr63ji react: 18.1.0 react-dom: 18.1.0_react@18.1.0 + tabbable: 6.1.1 transitivePeerDependencies: - '@types/react' dev: false - /@floating-ui/react-dom/1.0.0_ef5jwxihqo6n7gxfmzogljlgcm: - resolution: {integrity: sha512-uiOalFKPG937UCLm42RxjESTWUVpbbatvlphQAU6bsv+ence6IoVG8JOUZcy8eW81NkU+Idiwvx10WFLmR4MIg==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - dependencies: - '@floating-ui/dom': 1.0.4 - react: 18.1.0 - react-dom: 18.1.0_react@18.1.0 - dev: false - - /@hookform/error-message/2.0.0_l2dcsysovzdujulgxvsen7vbsm: + /@hookform/error-message/2.0.0_ie7afrsxjmqsdnv6azc5osehhy: resolution: {integrity: sha512-Y90nHzjgL2MP7GFy75kscdvxrCTjtyxGmOLLxX14nd08OXRIh9lMH/y9Kpdo0p1IPowJBiZMHyueg7p+yrqynQ==} peerDependencies: react: '>=16.8.0' @@ -2928,15 +918,15 @@ packages: dependencies: react: 18.1.0 react-dom: 18.1.0_react@18.1.0 - react-hook-form: 7.6.9_react@18.1.0 + react-hook-form: 7.43.1_react@18.1.0 dev: false - /@hookform/resolvers/2.8.9_react-hook-form@7.6.9: + /@hookform/resolvers/2.8.9_react-hook-form@7.43.1: resolution: {integrity: sha512-IXwGpjewxScF4N2kuyYDip6ABqH4lCg9n1f1mp0vbmKik+u+nestpbtdEs6U1WQZxwaoK/2APv1+MEr4czX7XA==} peerDependencies: react-hook-form: ^7.0.0 dependencies: - react-hook-form: 7.6.9_react@18.1.0 + react-hook-form: 7.43.1_react@18.1.0 dev: false /@humanwhocodes/config-array/0.9.5: @@ -2963,36 +953,27 @@ packages: get-package-type: 0.1.0 js-yaml: 3.14.1 resolve-from: 5.0.0 + dev: false /@istanbuljs/schema/0.1.3: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + dev: false - /@jest/console/28.1.1: - resolution: {integrity: sha512-0RiUocPVFEm3WRMOStIHbRWllG6iW6E3/gUPnf4lkrVFyXIIDeCe+vlKeYyFOMhB2EPE6FLFCNADSOOQMaqvyA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/console/29.5.0: + resolution: {integrity: sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 28.1.1 + '@jest/types': 29.5.0 '@types/node': 16.11.7 chalk: 4.1.2 - jest-message-util: 28.1.1 - jest-util: 28.1.1 + jest-message-util: 29.5.0 + jest-util: 29.5.0 slash: 3.0.0 dev: false - /@jest/console/29.0.3: - resolution: {integrity: sha512-cGg0r+klVHSYnfE977S9wmpuQ9L+iYuYgL+5bPXiUlUynLLYunRxswEmhBzvrSKGof5AKiHuTTmUKAqRcDY9dg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/types': 29.0.3 - '@types/node': 16.11.7 - chalk: 4.1.2 - jest-message-util: 29.0.3 - jest-util: 29.0.3 - slash: 3.0.0 - - /@jest/core/29.0.3_ts-node@10.8.1: - resolution: {integrity: sha512-1d0hLbOrM1qQE3eP3DtakeMbKTcXiXP3afWxqz103xPyddS2NhnNghS7MaXx1dcDt4/6p4nlhmeILo2ofgi8cQ==} + /@jest/core/29.5.0_ts-node@10.9.1: + resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -3000,37 +981,38 @@ packages: node-notifier: optional: true dependencies: - '@jest/console': 29.0.3 - '@jest/reporters': 29.0.3 - '@jest/test-result': 29.0.3 - '@jest/transform': 29.0.3 - '@jest/types': 29.0.3 + '@jest/console': 29.5.0 + '@jest/reporters': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 16.11.7 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.3.1 exit: 0.1.2 graceful-fs: 4.2.10 - jest-changed-files: 29.0.0 - jest-config: 29.0.3_yqiaopbgmqcuvx27p5xxvum6wm - jest-haste-map: 29.0.3 - jest-message-util: 29.0.3 - jest-regex-util: 29.0.0 - jest-resolve: 29.0.3 - jest-resolve-dependencies: 29.0.3 - jest-runner: 29.0.3 - jest-runtime: 29.0.3 - jest-snapshot: 29.0.3 - jest-util: 29.0.3 - jest-validate: 29.0.3 - jest-watcher: 29.0.3 + jest-changed-files: 29.5.0 + jest-config: 29.5.0_6m7kcbkkzjz4ln6z66tlzx44we + jest-haste-map: 29.5.0 + jest-message-util: 29.5.0 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-resolve-dependencies: 29.5.0 + jest-runner: 29.5.0 + jest-runtime: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 + jest-watcher: 29.5.0 micromatch: 4.0.5 - pretty-format: 29.0.3 + pretty-format: 29.5.0 slash: 3.0.0 strip-ansi: 6.0.1 transitivePeerDependencies: - supports-color - ts-node + dev: false /@jest/create-cache-key-function/27.5.1: resolution: {integrity: sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==} @@ -3039,14 +1021,14 @@ packages: '@jest/types': 27.5.1 dev: true - /@jest/environment/29.0.3: - resolution: {integrity: sha512-iKl272NKxYNQNqXMQandAIwjhQaGw5uJfGXduu8dS9llHi8jV2ChWrtOAVPnMbaaoDhnI3wgUGNDvZgHeEJQCA==} + /@jest/environment/29.5.0: + resolution: {integrity: sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/fake-timers': 29.0.3 - '@jest/types': 29.0.3 + '@jest/fake-timers': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 16.11.7 - jest-mock: 29.0.3 + jest-mock: 29.5.0 /@jest/expect-utils/29.0.3: resolution: {integrity: sha512-i1xUkau7K/63MpdwiRqaxgZOjxYs4f0WMTGJnYwUKubsNRZSeQbLorS7+I4uXVF9KQ5r61BUPAUMZ7Lf66l64Q==} @@ -3054,39 +1036,48 @@ packages: dependencies: jest-get-type: 29.0.0 - /@jest/expect/29.0.3: - resolution: {integrity: sha512-6W7K+fsI23FQ01H/BWccPyDZFrnU9QlzDcKOjrNVU5L8yUORFAJJIpmyxWPW70+X624KUNqzZwPThPMX28aXEQ==} + /@jest/expect-utils/29.5.0: + resolution: {integrity: sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - expect: 29.0.3 - jest-snapshot: 29.0.3 + jest-get-type: 29.4.3 + dev: false + + /@jest/expect/29.5.0: + resolution: {integrity: sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.5.0 + jest-snapshot: 29.5.0 transitivePeerDependencies: - supports-color + dev: false - /@jest/fake-timers/29.0.3: - resolution: {integrity: sha512-tmbUIo03x0TdtcZCESQ0oQSakPCpo7+s6+9mU19dd71MptkP4zCwoeZqna23//pgbhtT1Wq02VmA9Z9cNtvtCQ==} + /@jest/fake-timers/29.5.0: + resolution: {integrity: sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.0.3 - '@sinonjs/fake-timers': 9.1.2 + '@jest/types': 29.5.0 + '@sinonjs/fake-timers': 10.0.2 '@types/node': 16.11.7 - jest-message-util: 29.0.3 - jest-mock: 29.0.3 - jest-util: 29.0.3 + jest-message-util: 29.5.0 + jest-mock: 29.5.0 + jest-util: 29.5.0 - /@jest/globals/29.0.3: - resolution: {integrity: sha512-YqGHT65rFY2siPIHHFjuCGUsbzRjdqkwbat+Of6DmYRg5shIXXrLdZoVE/+TJ9O1dsKsFmYhU58JvIbZRU1Z9w==} + /@jest/globals/29.5.0: + resolution: {integrity: sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.0.3 - '@jest/expect': 29.0.3 - '@jest/types': 29.0.3 - jest-mock: 29.0.3 + '@jest/environment': 29.5.0 + '@jest/expect': 29.5.0 + '@jest/types': 29.5.0 + jest-mock: 29.5.0 transitivePeerDependencies: - supports-color + dev: false - /@jest/reporters/29.0.3: - resolution: {integrity: sha512-3+QU3d4aiyOWfmk1obDerie4XNCaD5Xo1IlKNde2yGEi02WQD+ZQD0i5Hgqm1e73sMV7kw6pMlCnprtEwEVwxw==} + /@jest/reporters/29.5.0: + resolution: {integrity: sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -3095,10 +1086,10 @@ packages: optional: true dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.0.3 - '@jest/test-result': 29.0.3 - '@jest/transform': 29.0.3 - '@jest/types': 29.0.3 + '@jest/console': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@jridgewell/trace-mapping': 0.3.15 '@types/node': 16.11.7 chalk: 4.1.2 @@ -3111,22 +1102,15 @@ packages: istanbul-lib-report: 3.0.0 istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.4 - jest-message-util: 29.0.3 - jest-util: 29.0.3 - jest-worker: 29.0.3 + jest-message-util: 29.5.0 + jest-util: 29.5.0 + jest-worker: 29.5.0 slash: 3.0.0 string-length: 4.0.2 strip-ansi: 6.0.1 - terminal-link: 2.1.1 v8-to-istanbul: 9.0.1 transitivePeerDependencies: - supports-color - - /@jest/schemas/28.0.2: - resolution: {integrity: sha512-YVDJZjd4izeTDkij00vHHAymNXQ6WWsdChFRK86qck6Jpr3DCL5W3Is3vslviRlP+bLuMYRLbdp98amMvqudhA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@sinclair/typebox': 0.23.5 dev: false /@jest/schemas/29.0.0: @@ -3135,63 +1119,73 @@ packages: dependencies: '@sinclair/typebox': 0.24.40 - /@jest/source-map/29.0.0: - resolution: {integrity: sha512-nOr+0EM8GiHf34mq2GcJyz/gYFyLQ2INDhAylrZJ9mMWoW21mLBfZa0BUVPPMxVYrLjeiRe2Z7kWXOGnS0TFhQ==} + /@jest/schemas/29.4.3: + resolution: {integrity: sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.25.24 + + /@jest/source-map/29.4.3: + resolution: {integrity: sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jridgewell/trace-mapping': 0.3.15 callsites: 3.1.0 graceful-fs: 4.2.10 - - /@jest/test-result/28.1.1: - resolution: {integrity: sha512-hPmkugBktqL6rRzwWAtp1JtYT4VHwv8OQ+9lE5Gymj6dHzubI/oJHMUpPOt8NrdVWSrz9S7bHjJUmv2ggFoUNQ==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@jest/console': 28.1.1 - '@jest/types': 28.1.1 - '@types/istanbul-lib-coverage': 2.0.3 - collect-v8-coverage: 1.0.1 dev: false /@jest/test-result/29.0.3: resolution: {integrity: sha512-vViVnQjCgTmbhDKEonKJPtcFe9G/CJO4/Np4XwYJah+lF2oI7KKeRp8t1dFvv44wN2NdbDb/qC6pi++Vpp0Dlg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.0.3 - '@jest/types': 29.0.3 + '@jest/console': 29.5.0 + '@jest/types': 29.5.0 '@types/istanbul-lib-coverage': 2.0.3 collect-v8-coverage: 1.0.1 + dev: false - /@jest/test-sequencer/29.0.3: - resolution: {integrity: sha512-Hf4+xYSWZdxTNnhDykr8JBs0yBN/nxOXyUQWfotBUqqy0LF9vzcFB0jm/EDNZCx587znLWTIgxcokW7WeZMobQ==} + /@jest/test-result/29.5.0: + resolution: {integrity: sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.0.3 - graceful-fs: 4.2.10 - jest-haste-map: 29.0.3 - slash: 3.0.0 + '@jest/console': 29.5.0 + '@jest/types': 29.5.0 + '@types/istanbul-lib-coverage': 2.0.3 + collect-v8-coverage: 1.0.1 + dev: false - /@jest/transform/29.0.3: - resolution: {integrity: sha512-C5ihFTRYaGDbi/xbRQRdbo5ddGtI4VSpmL6AIcZxdhwLbXMa7PcXxxqyI91vGOFHnn5aVM3WYnYKCHEqmLVGzg==} + /@jest/test-sequencer/29.5.0: + resolution: {integrity: sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.5.0 + graceful-fs: 4.2.10 + jest-haste-map: 29.5.0 + slash: 3.0.0 + dev: false + + /@jest/transform/29.5.0: + resolution: {integrity: sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.18.9 - '@jest/types': 29.0.3 + '@jest/types': 29.5.0 '@jridgewell/trace-mapping': 0.3.15 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 - convert-source-map: 1.7.0 + convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.10 - jest-haste-map: 29.0.3 - jest-regex-util: 29.0.0 - jest-util: 29.0.3 + jest-haste-map: 29.5.0 + jest-regex-util: 29.4.3 + jest-util: 29.5.0 micromatch: 4.0.5 pirates: 4.0.5 slash: 3.0.0 - write-file-atomic: 4.0.1 + write-file-atomic: 4.0.2 transitivePeerDependencies: - supports-color + dev: false /@jest/types/27.5.1: resolution: {integrity: sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==} @@ -3204,23 +1198,11 @@ packages: chalk: 4.1.2 dev: true - /@jest/types/28.1.1: - resolution: {integrity: sha512-vRXVqSg1VhDnB8bWcmvLzmg0Bt9CRKVgHPXqYwvWMX3TvAjeO+nRuK6+VdTKCtWOvYlmkF/HqNAL/z+N3B53Kw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@jest/schemas': 28.0.2 - '@types/istanbul-lib-coverage': 2.0.3 - '@types/istanbul-reports': 3.0.1 - '@types/node': 16.11.7 - '@types/yargs': 17.0.10 - chalk: 4.1.2 - dev: false - - /@jest/types/29.0.3: - resolution: {integrity: sha512-coBJmOQvurXjN1Hh5PzF7cmsod0zLIOXpP8KD161mqNlroMhLcwpODiEzi7ZsRl5Z/AIuxpeNm8DCl43F4kz8A==} + /@jest/types/29.5.0: + resolution: {integrity: sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 29.0.0 + '@jest/schemas': 29.4.3 '@types/istanbul-lib-coverage': 2.0.3 '@types/istanbul-reports': 3.0.1 '@types/node': 16.11.7 @@ -3233,6 +1215,7 @@ packages: dependencies: '@jridgewell/set-array': 1.1.1 '@jridgewell/sourcemap-codec': 1.4.13 + dev: false /@jridgewell/gen-mapping/0.3.2: resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} @@ -3360,8 +1343,8 @@ packages: - encoding dev: true - /@openapitools/openapi-generator-cli/2.5.1: - resolution: {integrity: sha512-WSRQBU0dCSVD+0Qv8iCsv0C4iMaZe/NpJ/CT4SmrEYLH3txoKTE8wEfbdj/kqShS8Or0YEGDPUzhSIKY151L0w==} + /@openapitools/openapi-generator-cli/2.5.2: + resolution: {integrity: sha512-FLgkjzpDiHVsH821db0VDSElDoA6TcspGyq3RD4zLBJaJhbSsRwr4u87sNoyuHKBg4OMJbZMT4iJxAhkosKrzw==} engines: {node: '>=10.0.0'} hasBin: true requiresBuild: true @@ -3426,132 +1409,126 @@ packages: reselect: 4.1.5 dev: false - /@rushstack/eslint-patch/1.1.3: - resolution: {integrity: sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==} - dev: true - - /@sinclair/typebox/0.23.5: - resolution: {integrity: sha512-AFBVi/iT4g20DHoujvMH1aEDn8fGJh4xsRGCP6d8RpLPMqsNPvW01Jcn0QysXTsg++/xj25NmJsGyH9xug/wKg==} - dev: false - /@sinclair/typebox/0.24.40: resolution: {integrity: sha512-Xint60L8rF0+nRy+6fCjW9jQMmu7fTpbwTBrXZiK6eq/RHDJS7LvWX/0oXC8O7fCePmrY/XdfaTv2HiUDeCq4g==} - /@sinonjs/commons/1.8.3: - resolution: {integrity: sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==} + /@sinclair/typebox/0.25.24: + resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} + + /@sinonjs/commons/2.0.0: + resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} dependencies: type-detect: 4.0.8 - /@sinonjs/fake-timers/9.1.2: - resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} + /@sinonjs/fake-timers/10.0.2: + resolution: {integrity: sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==} dependencies: - '@sinonjs/commons': 1.8.3 + '@sinonjs/commons': 2.0.0 - /@swc/core-darwin-arm64/1.3.22: - resolution: {integrity: sha512-MMhtPsuXp8gpUgr9bs+RZQ2IyFGiUNDG93usCDAFgAF+6VVp+YaAVjET/3/Bx5Lk2WAt0RxT62C9KTEw1YMo3w==} + /@swc/core-darwin-arm64/1.3.38: + resolution: {integrity: sha512-4ZTJJ/cR0EsXW5UxFCifZoGfzQ07a8s4ayt1nLvLQ5QoB1GTAf9zsACpvWG8e7cmCR0L76R5xt8uJuyr+noIXA==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] requiresBuild: true optional: true - /@swc/core-darwin-x64/1.3.22: - resolution: {integrity: sha512-SG6QbNat4GZ5VJU3Zo6a54oQOtbhJVE6BCQw4JjOCZJmAeBzNebGy9wsT4+fCJNHC3C5qtaRw7ToXJvLniXwfg==} + /@swc/core-darwin-x64/1.3.38: + resolution: {integrity: sha512-Kim727rNo4Dl8kk0CR8aJQe4zFFtsT1TZGlNrNMUgN1WC3CRX7dLZ6ZJi/VVcTG1cbHp5Fp3mUzwHsMxEh87Mg==} engines: {node: '>=10'} cpu: [x64] os: [darwin] requiresBuild: true optional: true - /@swc/core-linux-arm-gnueabihf/1.3.22: - resolution: {integrity: sha512-4E+TdQT1oHnHjDaPs/DyrRy9lOuFd6ncEd67yYA4j9lFqt6nuz/jnXss45k8KU7wR5kOTtdW73xPwkU4NbOWdw==} + /@swc/core-linux-arm-gnueabihf/1.3.38: + resolution: {integrity: sha512-yaRdnPNU2enlJDRcIMvYVSyodY+Amhf5QuXdUbAj6rkDD6wUs/s9C6yPYrFDmoTltrG+nBv72mUZj+R46wVfSw==} engines: {node: '>=10'} cpu: [arm] os: [linux] requiresBuild: true optional: true - /@swc/core-linux-arm64-gnu/1.3.22: - resolution: {integrity: sha512-6VcynOMbOBcbLutIPENI3Ejvg5LGz/Pwvzm25hM0FoiEtPxHA+tawQUwLx8Alk1Yr+Rnqid06UEZ0veJOGn2pQ==} + /@swc/core-linux-arm64-gnu/1.3.38: + resolution: {integrity: sha512-iNY1HqKo/wBSu3QOGBUlZaLdBP/EHcwNjBAqIzpb8J64q2jEN02RizqVW0mDxyXktJ3lxr3g7VW9uqklMeXbjQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] requiresBuild: true optional: true - /@swc/core-linux-arm64-musl/1.3.22: - resolution: {integrity: sha512-86RxGy0L3qa4De3xWHx8vL2caTxvSLSWTlgUW/Yd4l1pvrCFibMjhkImGu5ViKiReX9DlBtJ7CBs4dln2kHidw==} + /@swc/core-linux-arm64-musl/1.3.38: + resolution: {integrity: sha512-LJCFgLZoPRkPCPmux+Q5ctgXRp6AsWhvWuY61bh5bIPBDlaG9pZk94DeHyvtiwT0syhTtXb2LieBOx6NqN3zeA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] requiresBuild: true optional: true - /@swc/core-linux-x64-gnu/1.3.22: - resolution: {integrity: sha512-FLkbiqsdXsVIFZi6iedx4rSBGX8x0vo/5aDlklSxJAAYOcQpO0QADKP5Yr65iMT1d6ABCt2d+/StpGLF7GWOcA==} + /@swc/core-linux-x64-gnu/1.3.38: + resolution: {integrity: sha512-hRQGRIWHmv2PvKQM/mMV45mVXckM2+xLB8TYLLgUG66mmtyGTUJPyxjnJkbI86WNGqo18k+lAuMG2mn6QmzYwQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@swc/core-linux-x64-musl/1.3.22: - resolution: {integrity: sha512-giBuw+Z0Bq6fpZ0Y5TcfpcQwf9p/cE1fOQyO/K1XSTn/haQOqFi7421Jq/dFThSARZiXw1u9Om9VFbwxr8VI+A==} + /@swc/core-linux-x64-musl/1.3.38: + resolution: {integrity: sha512-PTYSqtsIfPHLKDDNbueI5e0sc130vyHRiFOeeC6qqzA2FAiVvIxuvXHLr0soPvKAR1WyhtYmFB9QarcctemL2w==} engines: {node: '>=10'} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@swc/core-win32-arm64-msvc/1.3.22: - resolution: {integrity: sha512-loKGO+ZM2By6VdrmVJk1G79jVgDPaee93qLFuis5KyeoLLb4m1MlNMc/6SIDZUSuYg6NqaGP1spFeiFetMQ4Zg==} + /@swc/core-win32-arm64-msvc/1.3.38: + resolution: {integrity: sha512-9lHfs5TPNs+QdkyZFhZledSmzBEbqml/J1rqPSb9Fy8zB6QlspixE6OLZ3nTlUOdoGWkcTTdrOn77Sd7YGf1AA==} engines: {node: '>=10'} cpu: [arm64] os: [win32] requiresBuild: true optional: true - /@swc/core-win32-ia32-msvc/1.3.22: - resolution: {integrity: sha512-lvNWAZ3QjXMsrsch6oLLQVikT/hC/4ZcLrTBXa14HwQylaYigkGElgp3ekJr78HjWDPwB46GXwBbNMG0VNAfvA==} + /@swc/core-win32-ia32-msvc/1.3.38: + resolution: {integrity: sha512-SbL6pfA2lqvDKnwTHwOfKWvfHAdcbAwJS4dBkFidr7BiPTgI5Uk8wAPcRb8mBECpmIa9yFo+N0cAFRvMnf+cNw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] requiresBuild: true optional: true - /@swc/core-win32-x64-msvc/1.3.22: - resolution: {integrity: sha512-ESyn4lZXAKEE3mcTaDfXatsolCiEfVGstsXdgBmZYa6o1IE1bDW8FE7Ob/Y+82WTpm9+A9ZYXYjZ62t67POHZg==} + /@swc/core-win32-x64-msvc/1.3.38: + resolution: {integrity: sha512-UFveLrL6eGvViOD8OVqUQa6QoQwdqwRvLtL5elF304OT8eCPZa8BhuXnWk25X8UcOyns8gFcb8Fhp3oaLi/Rlw==} engines: {node: '>=10'} cpu: [x64] os: [win32] requiresBuild: true optional: true - /@swc/core/1.3.22: - resolution: {integrity: sha512-oQ9EPEb7NgWcGIDoVfLCuffvtC4MzVtrwjqwKzFHP8FUh1fn8+2wraOjkkDXW74BB4Hgve5ykkaHix9bebB9Ww==} + /@swc/core/1.3.38: + resolution: {integrity: sha512-AiEVehRFws//AiiLx9DPDp1WDXt+yAoGD1kMYewhoF6QLdTz8AtYu6i8j/yAxk26L8xnegy0CDwcNnub9qenyQ==} engines: {node: '>=10'} - hasBin: true requiresBuild: true optionalDependencies: - '@swc/core-darwin-arm64': 1.3.22 - '@swc/core-darwin-x64': 1.3.22 - '@swc/core-linux-arm-gnueabihf': 1.3.22 - '@swc/core-linux-arm64-gnu': 1.3.22 - '@swc/core-linux-arm64-musl': 1.3.22 - '@swc/core-linux-x64-gnu': 1.3.22 - '@swc/core-linux-x64-musl': 1.3.22 - '@swc/core-win32-arm64-msvc': 1.3.22 - '@swc/core-win32-ia32-msvc': 1.3.22 - '@swc/core-win32-x64-msvc': 1.3.22 + '@swc/core-darwin-arm64': 1.3.38 + '@swc/core-darwin-x64': 1.3.38 + '@swc/core-linux-arm-gnueabihf': 1.3.38 + '@swc/core-linux-arm64-gnu': 1.3.38 + '@swc/core-linux-arm64-musl': 1.3.38 + '@swc/core-linux-x64-gnu': 1.3.38 + '@swc/core-linux-x64-musl': 1.3.38 + '@swc/core-win32-arm64-msvc': 1.3.38 + '@swc/core-win32-ia32-msvc': 1.3.38 + '@swc/core-win32-x64-msvc': 1.3.38 - /@swc/jest/0.2.24_@swc+core@1.3.22: + /@swc/jest/0.2.24_@swc+core@1.3.38: resolution: {integrity: sha512-fwgxQbM1wXzyKzl1+IW0aGrRvAA8k0Y3NxFhKigbPjOJ4mCKnWEcNX9HQS3gshflcxq8YKhadabGUVfdwjCr6Q==} engines: {npm: '>= 7.0.0'} peerDependencies: '@swc/core': '*' dependencies: '@jest/create-cache-key-function': 27.5.1 - '@swc/core': 1.3.22 + '@swc/core': 1.3.38 jsonc-parser: 3.2.0 dev: true @@ -3611,7 +1588,7 @@ packages: resolution: {integrity: sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ==} engines: {node: '>=12'} dependencies: - '@babel/code-frame': 7.16.7 + '@babel/code-frame': 7.18.6 '@babel/runtime': 7.17.9 '@types/aria-query': 4.2.2 aria-query: 5.0.0 @@ -3619,43 +1596,57 @@ packages: dom-accessibility-api: 0.5.10 lz-string: 1.4.4 pretty-format: 27.5.1 + dev: true - /@testing-library/jest-dom/5.16.4: - resolution: {integrity: sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==} + /@testing-library/dom/9.0.0: + resolution: {integrity: sha512-+/TLgKNFsYUshOY/zXsQOk+PlFQK+eyJ9T13IDVNJEi+M+Un7xlJK+FZKkbGSnf0+7E1G6PlDhkSYQ/GFiruBQ==} + engines: {node: '>=14'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/runtime': 7.17.9 + '@types/aria-query': 5.0.1 + aria-query: 5.0.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.10 + lz-string: 1.4.4 + pretty-format: 27.5.1 + + /@testing-library/jest-dom/5.16.5: + resolution: {integrity: sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==} engines: {node: '>=8', npm: '>=6', yarn: '>=1'} dependencies: + '@adobe/css-tools': 4.2.0 '@babel/runtime': 7.17.9 '@types/testing-library__jest-dom': 5.14.5 aria-query: 5.0.0 chalk: 3.0.0 - css: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.5.10 lodash: 4.17.21 redent: 3.0.0 dev: true - /@testing-library/react/13.2.0_ef5jwxihqo6n7gxfmzogljlgcm: - resolution: {integrity: sha512-Bprbz/SZVONCJy5f7hcihNCv313IJXdYiv0nSJklIs1SQCIHHNlnGNkosSXnGZTmesyGIcBGNppYhXcc11pb7g==} - engines: {node: '>=12'} + /@testing-library/react/14.0.0_ef5jwxihqo6n7gxfmzogljlgcm: + resolution: {integrity: sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==} + engines: {node: '>=14'} peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 dependencies: '@babel/runtime': 7.17.9 - '@testing-library/dom': 8.13.0 + '@testing-library/dom': 9.0.0 '@types/react-dom': 18.0.5 react: 18.1.0 react-dom: 18.1.0_react@18.1.0 dev: false - /@testing-library/user-event/14.4.3_tlwynutqiyp5mns3woioasuxnq: + /@testing-library/user-event/14.4.3_@testing-library+dom@9.0.0: resolution: {integrity: sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==} engines: {node: '>=12', npm: '>=6'} peerDependencies: '@testing-library/dom': '>=7.21.4' dependencies: - '@testing-library/dom': 8.13.0 + '@testing-library/dom': 9.0.0 dev: true /@tootallnate/once/2.0.0: @@ -3686,6 +1677,10 @@ packages: /@types/aria-query/4.2.2: resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==} + dev: true + + /@types/aria-query/5.0.1: + resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} /@types/babel__core/7.1.19: resolution: {integrity: sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==} @@ -3695,22 +1690,26 @@ packages: '@types/babel__generator': 7.6.4 '@types/babel__template': 7.4.1 '@types/babel__traverse': 7.17.1 + dev: false /@types/babel__generator/7.6.4: resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} dependencies: '@babel/types': 7.18.9 + dev: false /@types/babel__template/7.4.1: resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} dependencies: '@babel/parser': 7.18.9 '@babel/types': 7.18.9 + dev: false /@types/babel__traverse/7.17.1: resolution: {integrity: sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA==} dependencies: '@babel/types': 7.18.9 + dev: false /@types/eventsource/1.1.8: resolution: {integrity: sha512-fJQNt9LijJCZwYvM6O30uLzdpAK9zs52Uc9iUW9M2Zsg0HQM6DLf6QysjC/wuFX+0798B8AppVMvgdO6IftPKQ==} @@ -3720,6 +1719,7 @@ packages: resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} dependencies: '@types/node': 16.11.7 + dev: false /@types/history/4.7.11: resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} @@ -3768,6 +1768,7 @@ packages: /@types/lodash/4.14.177: resolution: {integrity: sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==} + dev: true /@types/node/16.11.7: resolution: {integrity: sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==} @@ -3778,12 +1779,13 @@ packages: /@types/prettier/2.6.3: resolution: {integrity: sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg==} + dev: false /@types/prop-types/15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} - /@types/react-datepicker/4.4.2_react@18.1.0: - resolution: {integrity: sha512-g8DhWvYmaIMLzVrIEVLXncylyImyBaoPsEUr3yR13JDaaHoebhDorqnVv4tLkNGa8SjBB8SAOQvxD5jaPNBX8A==} + /@types/react-datepicker/4.10.0_react@18.1.0: + resolution: {integrity: sha512-Cq+ks20vBIU6XN67TbkCHu8M7V46Y6vJrKE2n+8q/GfueJyWWTIKeC3Z7cz/d+qxGDq/VCrqA929R0U4lNuztg==} dependencies: '@popperjs/core': 2.9.2 '@types/react': 18.0.9 @@ -3888,19 +1890,6 @@ packages: - supports-color dev: true - /@typescript-eslint/experimental-utils/5.23.0_vjep2yp2sits3sqnodefgcbnfi: - resolution: {integrity: sha512-I+3YGQztH1DM9kgWzjslpZzJCBMRz0KhYG2WP62IwpooeZ1L6Qt0mNK8zs+uP+R2HOsr+TeDW35Pitc3PfVv8Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@typescript-eslint/utils': 5.23.0_vjep2yp2sits3sqnodefgcbnfi - eslint: 8.16.0 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - /@typescript-eslint/parser/5.29.0_vjep2yp2sits3sqnodefgcbnfi: resolution: {integrity: sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3921,14 +1910,6 @@ packages: - supports-color dev: true - /@typescript-eslint/scope-manager/5.23.0: - resolution: {integrity: sha512-EhjaFELQHCRb5wTwlGsNMvzK9b8Oco4aYNleeDlNuL6qXWDF47ch4EhVNPh8Rdhf9tmqbN4sWDk/8g+Z/J8JVw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.23.0 - '@typescript-eslint/visitor-keys': 5.23.0 - dev: true - /@typescript-eslint/scope-manager/5.29.0: resolution: {integrity: sha512-etbXUT0FygFi2ihcxDZjz21LtC+Eps9V2xVx09zFoN44RRHPrkMflidGMI+2dUs821zR1tDS6Oc9IXxIjOUZwA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3956,37 +1937,11 @@ packages: - supports-color dev: true - /@typescript-eslint/types/5.23.0: - resolution: {integrity: sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - /@typescript-eslint/types/5.29.0: resolution: {integrity: sha512-X99VbqvAXOMdVyfFmksMy3u8p8yoRGITgU1joBJPzeYa0rhdf5ok9S56/itRoUSh99fiDoMtarSIJXo7H/SnOg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/typescript-estree/5.23.0_typescript@4.7.4: - resolution: {integrity: sha512-xE9e0lrHhI647SlGMl+m+3E3CKPF1wzvvOEWnuE3CCjjT7UiRnDGJxmAcVKJIlFgK6DY9RB98eLr1OPigPEOGg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 5.23.0 - '@typescript-eslint/visitor-keys': 5.23.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.3.7 - tsutils: 3.21.0_typescript@4.7.4 - typescript: 4.7.4 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/typescript-estree/5.29.0_typescript@4.7.4: resolution: {integrity: sha512-mQvSUJ/JjGBdvo+1LwC+GY2XmSYjK1nAaVw2emp/E61wEVYEyibRHCqm1I1vEKbXCpUKuW4G7u9ZCaZhJbLoNQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4008,24 +1963,6 @@ packages: - supports-color dev: true - /@typescript-eslint/utils/5.23.0_vjep2yp2sits3sqnodefgcbnfi: - resolution: {integrity: sha512-dbgaKN21drqpkbbedGMNPCtRPZo1IOUr5EI9Jrrh99r5UW5Q0dz46RKXeSBoPV+56R6dFKpbrdhgUNSJsDDRZA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@types/json-schema': 7.0.11 - '@typescript-eslint/scope-manager': 5.23.0 - '@typescript-eslint/types': 5.23.0 - '@typescript-eslint/typescript-estree': 5.23.0_typescript@4.7.4 - eslint: 8.16.0 - eslint-scope: 5.1.1 - eslint-utils: 3.0.0_eslint@8.16.0 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - /@typescript-eslint/utils/5.29.0_vjep2yp2sits3sqnodefgcbnfi: resolution: {integrity: sha512-3Eos6uP1nyLOBayc/VUdKZikV90HahXE5Dx9L5YlSd/7ylQPXhLk1BYb29SDgnBnTp+jmSZUU0QxUiyHgW4p7A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4044,14 +1981,6 @@ packages: - typescript dev: true - /@typescript-eslint/visitor-keys/5.23.0: - resolution: {integrity: sha512-Vd4mFNchU62sJB8pX19ZSPog05B0Y0CE2UxAZPT5k4iqhRYjPnqyY3woMxCd0++t9OTqkgjST+1ydLBi7e2Fvg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.23.0 - eslint-visitor-keys: 3.3.0 - dev: true - /@typescript-eslint/visitor-keys/5.29.0: resolution: {integrity: sha512-Hpb/mCWsjILvikMQoZIE3voc9wtQcS0A9FUw3h8bhr9UxBdtI/tw1ZDZUOXHXLOVMedKCH5NxyzATwnU78bWCQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4065,7 +1994,7 @@ packages: peerDependencies: vite: ^4.0.0 dependencies: - '@swc/core': 1.3.22 + '@swc/core': 1.3.38 vite: 4.0.0_binvsr2w2vzqveeue6ibz2djda dev: true @@ -4121,18 +2050,8 @@ packages: - supports-color dev: true - /aggregate-error/3.1.0: - resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} - engines: {node: '>=8'} - dependencies: - clean-stack: 2.2.0 - indent-string: 4.0.0 - dev: true - - /ajv-formats/2.1.1_ajv@8.8.2: + /ajv-formats/2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 peerDependenciesMeta: ajv: optional: true @@ -4164,11 +2083,11 @@ packages: dependencies: type-fest: 0.21.3 - /ansi-escapes/5.0.0: - resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==} - engines: {node: '>=12'} + /ansi-escapes/6.0.0: + resolution: {integrity: sha512-IG23inYII3dWlU2EyiAiGj6Bwal5GzsgPMwjYGvc1HPE2dgbj4ZB5ToWBKSquKw74nB3TIuOwaI6/jSULzfgrw==} + engines: {node: '>=14.16'} dependencies: - type-fest: 1.4.0 + type-fest: 3.6.1 dev: false /ansi-regex/5.0.1: @@ -4178,6 +2097,7 @@ packages: /ansi-regex/6.0.1: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} engines: {node: '>=12'} + dev: false /ansi-styles/3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} @@ -4195,11 +2115,6 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - /ansi-styles/6.1.0: - resolution: {integrity: sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==} - engines: {node: '>=12'} - dev: true - /anymatch/3.1.2: resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} engines: {node: '>= 8'} @@ -4214,6 +2129,7 @@ packages: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: sprintf-js: 1.0.3 + dev: false /argparse/2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -4286,11 +2202,6 @@ packages: resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} dev: true - /astral-regex/2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} - dev: true - /async/3.2.4: resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} dev: true @@ -4299,12 +2210,6 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: true - /atob/2.1.2: - resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} - engines: {node: '>= 4.5.0'} - hasBin: true - dev: true - /axe-core/4.3.5: resolution: {integrity: sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA==} engines: {node: '>=4'} @@ -4322,17 +2227,17 @@ packages: resolution: {integrity: sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==} dev: true - /babel-jest/29.0.3_@babel+core@7.18.2: - resolution: {integrity: sha512-ApPyHSOhS/sVzwUOQIWJmdvDhBsMG01HX9z7ogtkp1TToHGGUWFlnXJUIzCgKPSfiYLn3ibipCYzsKSURHEwLg==} + /babel-jest/29.5.0_@babel+core@7.18.9: + resolution: {integrity: sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: - '@babel/core': 7.18.2 - '@jest/transform': 29.0.3 + '@babel/core': 7.18.9 + '@jest/transform': 29.5.0 '@types/babel__core': 7.1.19 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.0.2_@babel+core@7.18.2 + babel-preset-jest: 29.5.0_@babel+core@7.18.9 chalk: 4.1.2 graceful-fs: 4.2.10 slash: 3.0.0 @@ -4340,29 +2245,6 @@ packages: - supports-color dev: false - /babel-jest/29.0.3_@babel+core@7.18.9: - resolution: {integrity: sha512-ApPyHSOhS/sVzwUOQIWJmdvDhBsMG01HX9z7ogtkp1TToHGGUWFlnXJUIzCgKPSfiYLn3ibipCYzsKSURHEwLg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.8.0 - dependencies: - '@babel/core': 7.18.9 - '@jest/transform': 29.0.3 - '@types/babel__core': 7.1.19 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.0.2_@babel+core@7.18.9 - chalk: 4.1.2 - graceful-fs: 4.2.10 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - - /babel-plugin-dynamic-import-node/2.3.3: - resolution: {integrity: sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==} - dependencies: - object.assign: 4.1.2 - dev: true - /babel-plugin-istanbul/6.1.1: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} engines: {node: '>=8'} @@ -4374,96 +2256,17 @@ packages: test-exclude: 6.0.0 transitivePeerDependencies: - supports-color + dev: false - /babel-plugin-jest-hoist/29.0.2: - resolution: {integrity: sha512-eBr2ynAEFjcebVvu8Ktx580BD1QKCrBG1XwEUTXJe285p9HA/4hOhfWCFRQhTKSyBV0VzjhG7H91Eifz9s29hg==} + /babel-plugin-jest-hoist/29.5.0: + resolution: {integrity: sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/template': 7.18.6 '@babel/types': 7.18.9 '@types/babel__core': 7.1.19 '@types/babel__traverse': 7.17.1 - - /babel-plugin-macros/3.1.0: - resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} - engines: {node: '>=10', npm: '>=6'} - dependencies: - '@babel/runtime': 7.17.9 - cosmiconfig: 7.0.1 - resolve: 1.22.1 - dev: true - - /babel-plugin-polyfill-corejs2/0.3.1_@babel+core@7.18.2: - resolution: {integrity: sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.18.8 - '@babel/core': 7.18.2 - '@babel/helper-define-polyfill-provider': 0.3.1_@babel+core@7.18.2 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true - - /babel-plugin-polyfill-corejs2/0.3.1_@babel+core@7.18.9: - resolution: {integrity: sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.18.8 - '@babel/core': 7.18.9 - '@babel/helper-define-polyfill-provider': 0.3.1_@babel+core@7.18.9 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true - - /babel-plugin-polyfill-corejs3/0.5.2_@babel+core@7.18.2: - resolution: {integrity: sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-define-polyfill-provider': 0.3.1_@babel+core@7.18.2 - core-js-compat: 3.22.5 - transitivePeerDependencies: - - supports-color - dev: true - - /babel-plugin-polyfill-corejs3/0.5.2_@babel+core@7.18.9: - resolution: {integrity: sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-define-polyfill-provider': 0.3.1_@babel+core@7.18.9 - core-js-compat: 3.22.5 - transitivePeerDependencies: - - supports-color - dev: true - - /babel-plugin-polyfill-regenerator/0.3.1_@babel+core@7.18.2: - resolution: {integrity: sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.2 - '@babel/helper-define-polyfill-provider': 0.3.1_@babel+core@7.18.2 - transitivePeerDependencies: - - supports-color - dev: true - - /babel-plugin-polyfill-regenerator/0.3.1_@babel+core@7.18.9: - resolution: {integrity: sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.18.9 - '@babel/helper-define-polyfill-provider': 0.3.1_@babel+core@7.18.9 - transitivePeerDependencies: - - supports-color - dev: true + dev: false /babel-plugin-styled-components/1.13.2_styled-components@5.3.1: resolution: {integrity: sha512-Vb1R3d4g+MUfPQPVDMCGjm3cDocJEUTR7Xq7QS95JWWeksN1wdFRYpD2kulDgI3Huuaf1CZd+NK4KQmqUFh5dA==} @@ -4479,30 +2282,6 @@ packages: /babel-plugin-syntax-jsx/6.18.0: resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==} - /babel-plugin-transform-react-remove-prop-types/0.4.24: - resolution: {integrity: sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==} - dev: true - - /babel-preset-current-node-syntax/1.0.1_@babel+core@7.18.2: - resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.18.2 - '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.18.2 - '@babel/plugin-syntax-bigint': 7.8.3_@babel+core@7.18.2 - '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.18.2 - '@babel/plugin-syntax-import-meta': 7.10.4_@babel+core@7.18.2 - '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.18.2 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.18.2 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.18.2 - '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.18.2 - '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.18.2 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.18.2 - '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.18.2 - '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.18.2 - dev: false - /babel-preset-current-node-syntax/1.0.1_@babel+core@7.18.9: resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} peerDependencies: @@ -4521,51 +2300,18 @@ packages: '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.18.9 '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.18.9 '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.18.9 - - /babel-preset-jest/29.0.2_@babel+core@7.18.2: - resolution: {integrity: sha512-BeVXp7rH5TK96ofyEnHjznjLMQ2nAeDJ+QzxKnHAAMs0RgrQsCywjAN8m4mOm5Di0pxU//3AoEeJJrerMH5UeA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.18.2 - babel-plugin-jest-hoist: 29.0.2 - babel-preset-current-node-syntax: 1.0.1_@babel+core@7.18.2 dev: false - /babel-preset-jest/29.0.2_@babel+core@7.18.9: - resolution: {integrity: sha512-BeVXp7rH5TK96ofyEnHjznjLMQ2nAeDJ+QzxKnHAAMs0RgrQsCywjAN8m4mOm5Di0pxU//3AoEeJJrerMH5UeA==} + /babel-preset-jest/29.5.0_@babel+core@7.18.9: + resolution: {integrity: sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.18.9 - babel-plugin-jest-hoist: 29.0.2 + babel-plugin-jest-hoist: 29.5.0 babel-preset-current-node-syntax: 1.0.1_@babel+core@7.18.9 - - /babel-preset-react-app/10.0.1: - resolution: {integrity: sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==} - dependencies: - '@babel/core': 7.18.9 - '@babel/plugin-proposal-class-properties': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-proposal-decorators': 7.17.9_@babel+core@7.18.9 - '@babel/plugin-proposal-nullish-coalescing-operator': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-proposal-numeric-separator': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-proposal-optional-chaining': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-proposal-private-methods': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-proposal-private-property-in-object': 7.17.12_@babel+core@7.18.9 - '@babel/plugin-transform-flow-strip-types': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-transform-react-display-name': 7.16.7_@babel+core@7.18.9 - '@babel/plugin-transform-runtime': 7.17.10_@babel+core@7.18.9 - '@babel/preset-env': 7.18.2_@babel+core@7.18.9 - '@babel/preset-react': 7.17.12_@babel+core@7.18.9 - '@babel/preset-typescript': 7.17.12_@babel+core@7.18.9 - '@babel/runtime': 7.17.9 - babel-plugin-macros: 3.1.0 - babel-plugin-transform-react-remove-prop-types: 0.4.24 - transitivePeerDependencies: - - supports-color - dev: true + dev: false /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -4618,14 +2364,17 @@ packages: escalade: 3.1.1 node-releases: 2.0.5 picocolors: 1.0.0 + dev: false /bser/2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} dependencies: node-int64: 0.4.0 + dev: false /buffer-from/1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: false /buffer/5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -4652,16 +2401,19 @@ packages: /camelcase/5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} + dev: false /camelcase/6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} + dev: false /camelize/1.0.0: resolution: {integrity: sha512-W2lPwkBkMZwFlPCXhIlYgxu+7gC/NUlCtdK652DAJ1JdgV0sTrvuPFshNPrFa1TY2JOkLhgdeEBplB4ezEa+xg==} /caniuse-lite/1.0.30001352: resolution: {integrity: sha512-GUgH8w6YergqPQDGWhJGt8GDRnY0L/iJVQcU3eJ46GYf52R8tk0Wxp0PymuFVZboJYXGiCqwozAYZNRjVj6IcA==} + dev: false /chalk/2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -4686,20 +2438,21 @@ packages: ansi-styles: 4.3.0 supports-color: 7.2.0 + /chalk/5.2.0: + resolution: {integrity: sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: false + /char-regex/1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + dev: false /char-regex/2.0.1: resolution: {integrity: sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==} engines: {node: '>=12.20'} dev: false - /charcodes/0.2.0: - resolution: {integrity: sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ==} - engines: {node: '>=6'} - dev: true - /chardet/0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true @@ -4723,16 +2476,12 @@ packages: /cjs-module-lexer/1.2.2: resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} + dev: false /classnames/2.3.1: resolution: {integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==} dev: false - /clean-stack/2.2.0: - resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} - engines: {node: '>=6'} - dev: true - /cli-cursor/3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -4745,22 +2494,6 @@ packages: engines: {node: '>=6'} dev: true - /cli-truncate/2.1.0: - resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} - engines: {node: '>=8'} - dependencies: - slice-ansi: 3.0.0 - string-width: 4.2.3 - dev: true - - /cli-truncate/3.1.0: - resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - slice-ansi: 5.0.0 - string-width: 5.1.2 - dev: true - /cli-width/3.0.0: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} @@ -4781,6 +2514,7 @@ packages: /co/4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: false /code-block-writer/11.0.1: resolution: {integrity: sha512-0ch9DeCY8v/BWA9n1/Qu1ALG3lpesel4PYL2eNlGLgvGl+J7k74i+dSXSF3wLvF5SYII8/GUT/Ic+fycBR/DUQ==} @@ -4788,6 +2522,7 @@ packages: /collect-v8-coverage/1.0.1: resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} + dev: false /color-convert/1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -4806,10 +2541,6 @@ packages: /color-name/1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - /colorette/2.0.19: - resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} - dev: true - /combined-stream/1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -4827,11 +2558,6 @@ packages: engines: {node: '>= 12'} dev: true - /commander/9.3.0: - resolution: {integrity: sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==} - engines: {node: ^12.20.0 || >=14} - dev: true - /compare-versions/4.1.3: resolution: {integrity: sha512-WQfnbDcrYnGr55UwbxKiQKASnTtNnaAWVi8jZyy8NTpVAXWACSne8lMD1iaIo9AiU6mnuLvSVshCzewVuWxHUg==} dev: true @@ -4863,7 +2589,7 @@ packages: dev: true /console.table/0.10.0: - resolution: {integrity: sha1-CRcCVYiHW+/XDPLv9L7yxuLXXQQ=} + resolution: {integrity: sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==} engines: {node: '> 0.10'} dependencies: easy-table: 1.1.0 @@ -4873,13 +2599,11 @@ packages: resolution: {integrity: sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==} dependencies: safe-buffer: 5.1.2 + dev: false - /core-js-compat/3.22.5: - resolution: {integrity: sha512-rEF75n3QtInrYICvJjrAgV03HwKiYvtKHdPtaba1KucG+cNZ4NJnH9isqt979e67KZlhpbCOTwnsvnIr+CVeOg==} - dependencies: - browserslist: 4.20.4 - semver: 7.0.0 - dev: true + /convert-source-map/2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: false /core-js-pure/3.14.0: resolution: {integrity: sha512-YVh+LN2FgNU0odThzm61BsdkwrbrchumFq3oztnE9vTKC4KS2fvnPmcx8t6jnqAyOTCTF4ZSiuK8Qhh7SNcL4g==} @@ -4930,14 +2654,6 @@ packages: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} dev: true - /css/3.0.0: - resolution: {integrity: sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==} - dependencies: - inherits: 2.0.4 - source-map: 0.6.1 - source-map-resolve: 0.6.0 - dev: true - /cssom/0.3.8: resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} dev: true @@ -5022,13 +2738,9 @@ packages: resolution: {integrity: sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==} dev: true - /decode-uri-component/0.2.0: - resolution: {integrity: sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==} - engines: {node: '>=0.10'} - dev: true - /dedent/0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + dev: false /deep-is/0.1.3: resolution: {integrity: sha512-GtxAN4HvBachZzm4OnWqc45ESpUCMwkYcsjnsPs23FwJbsO+k4t0k9bQCgOmzIlpHO28+WPK/KRbRk0DDHuuDw==} @@ -5037,6 +2749,7 @@ packages: /deepmerge/4.2.2: resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} engines: {node: '>=0.10.0'} + dev: false /defaults/1.0.3: resolution: {integrity: sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==} @@ -5065,6 +2778,7 @@ packages: /detect-newline/3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} + dev: false /diff-match-patch/1.0.5: resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} @@ -5074,6 +2788,11 @@ packages: resolution: {integrity: sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + /diff-sequences/29.4.3: + resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false + /diff/4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -5114,12 +2833,8 @@ packages: engines: {node: '>=12'} dev: true - /eastasianwidth/0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - dev: true - /easy-table/1.1.0: - resolution: {integrity: sha1-hvmrTBAvA3G3KXuSplHVgkvIy3M=} + resolution: {integrity: sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==} optionalDependencies: wcwidth: 1.0.1 dev: true @@ -5134,10 +2849,17 @@ packages: /electron-to-chromium/1.4.151: resolution: {integrity: sha512-XaG2LpZi9fdiWYOqJh0dJy4SlVywCvpgYXhzOlZTp4JqSKqxn5URqOjbm9OMYB3aInA2GuHQiem1QUOc1yT0Pw==} + dev: false /emittery/0.10.2: resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==} engines: {node: '>=12'} + dev: false + + /emittery/0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + dev: false /emoji-regex/8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -5326,41 +3048,6 @@ packages: eslint: 8.16.0 dev: true - /eslint-config-react-app/7.0.1_qmzb7mn5nafzn657qxusdnr23a: - resolution: {integrity: sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==} - engines: {node: '>=14.0.0'} - peerDependencies: - eslint: ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@babel/core': 7.18.2 - '@babel/eslint-parser': 7.17.0_j4uj5cgi2mksbox6kqvi7jrs6u - '@rushstack/eslint-patch': 1.1.3 - '@typescript-eslint/eslint-plugin': 5.29.0_uaxwak76nssfibsnotx5epygnu - '@typescript-eslint/parser': 5.29.0_vjep2yp2sits3sqnodefgcbnfi - babel-preset-react-app: 10.0.1 - confusing-browser-globals: 1.0.11 - eslint: 8.16.0 - eslint-plugin-flowtype: 8.0.3_lkudaqqzr27bp3dduyr3eevxq4 - eslint-plugin-import: 2.26.0_h5azci6ujakbaa2xblg2jlxooy - eslint-plugin-jest: 25.7.0_76upbxnyphyptf6rdjdsihqkzi - eslint-plugin-jsx-a11y: 6.5.1_eslint@8.16.0 - eslint-plugin-react: 7.30.1_eslint@8.16.0 - eslint-plugin-react-hooks: 4.5.0_eslint@8.16.0 - eslint-plugin-testing-library: 5.5.0_vjep2yp2sits3sqnodefgcbnfi - typescript: 4.7.4 - transitivePeerDependencies: - - '@babel/plugin-syntax-flow' - - '@babel/plugin-transform-react-jsx' - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - jest - - supports-color - dev: true - /eslint-import-resolver-node/0.3.6: resolution: {integrity: sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==} dependencies: @@ -5417,21 +3104,6 @@ packages: - supports-color dev: true - /eslint-plugin-flowtype/8.0.3_lkudaqqzr27bp3dduyr3eevxq4: - resolution: {integrity: sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@babel/plugin-syntax-flow': ^7.14.5 - '@babel/plugin-transform-react-jsx': ^7.14.9 - eslint: ^8.1.0 - dependencies: - '@babel/plugin-syntax-flow': 7.18.6_@babel+core@7.18.2 - '@babel/plugin-transform-react-jsx': 7.18.6_@babel+core@7.18.2 - eslint: 8.16.0 - lodash: 4.17.21 - string-natural-compare: 3.0.1 - dev: true - /eslint-plugin-import/2.26.0_h5azci6ujakbaa2xblg2jlxooy: resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==} engines: {node: '>=4'} @@ -5463,8 +3135,8 @@ packages: - supports-color dev: true - /eslint-plugin-jest-dom/4.0.2_eslint@8.16.0: - resolution: {integrity: sha512-Jo51Atwyo2TdcUncjmU+UQeSTKh3sc2LF/M5i/R3nTU0Djw9V65KGJisdm/RtuKhy2KH/r7eQ1n6kwYFPNdHlA==} + /eslint-plugin-jest-dom/4.0.3_eslint@8.16.0: + resolution: {integrity: sha512-9j+n8uj0+V0tmsoS7bYC7fLhQmIvjRqRYEcbDSi+TKPsTThLLXCyj5swMSSf/hTleeMktACnn+HFqXBr5gbcbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6', yarn: '>=1'} peerDependencies: eslint: ^6.8.0 || ^7.0.0 || ^8.0.0 @@ -5475,28 +3147,6 @@ packages: requireindex: 1.2.0 dev: true - /eslint-plugin-jest/25.7.0_76upbxnyphyptf6rdjdsihqkzi: - resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/eslint-plugin': ^4.0.0 || ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - jest: '*' - peerDependenciesMeta: - '@typescript-eslint/eslint-plugin': - optional: true - jest: - optional: true - dependencies: - '@typescript-eslint/eslint-plugin': 5.29.0_uaxwak76nssfibsnotx5epygnu - '@typescript-eslint/experimental-utils': 5.23.0_vjep2yp2sits3sqnodefgcbnfi - eslint: 8.16.0 - jest: 29.0.3_yqiaopbgmqcuvx27p5xxvum6wm - transitivePeerDependencies: - - supports-color - - typescript - dev: true - /eslint-plugin-jsx-a11y/6.5.1_eslint@8.16.0: resolution: {integrity: sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g==} engines: {node: '>=4.0'} @@ -5518,7 +3168,7 @@ packages: minimatch: 3.1.2 dev: true - /eslint-plugin-prettier/4.0.0_q7a4ir2sdihdzpzdlnbgmzjlpq: + /eslint-plugin-prettier/4.0.0_cu7djixscogveocpaizwew7lgy: resolution: {integrity: sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==} engines: {node: '>=6.0.0'} peerDependencies: @@ -5531,7 +3181,7 @@ packages: dependencies: eslint: 8.16.0 eslint-config-prettier: 8.5.0_eslint@8.16.0 - prettier: 2.5.1 + prettier: 2.8.4 prettier-linter-helpers: 1.0.0 dev: true @@ -5567,19 +3217,6 @@ packages: string.prototype.matchall: 4.0.7 dev: true - /eslint-plugin-testing-library/5.5.0_vjep2yp2sits3sqnodefgcbnfi: - resolution: {integrity: sha512-eWQ19l6uWL7LW8oeMyQVSGjVYFnBqk7DMHjadm0yOHBvX3Xi9OBrsNuxoAMdX4r7wlQ5WWpW46d+CB6FWFL/PQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} - peerDependencies: - eslint: ^7.5.0 || ^8.0.0 - dependencies: - '@typescript-eslint/utils': 5.29.0_vjep2yp2sits3sqnodefgcbnfi - eslint: 8.16.0 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - /eslint-scope/5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -5716,25 +3353,12 @@ packages: onetime: 5.1.2 signal-exit: 3.0.7 strip-final-newline: 2.0.0 - - /execa/6.1.0: - resolution: {integrity: sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 3.0.1 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.1.0 - onetime: 6.0.0 - signal-exit: 3.0.7 - strip-final-newline: 3.0.0 - dev: true + dev: false /exit/0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} + dev: false /expect/29.0.3: resolution: {integrity: sha512-t8l5DTws3212VbmPL+tBFXhjRHLmctHB0oQbL8eUc6S7NzZtYUhycrFO9mkxA0ZUC6FAWdNi7JchJSkODtcu1Q==} @@ -5746,6 +3370,17 @@ packages: jest-message-util: 29.0.3 jest-util: 29.0.3 + /expect/29.5.0: + resolution: {integrity: sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.5.0 + jest-get-type: 29.4.3 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-util: 29.5.0 + dev: false + /external-editor/3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -5794,6 +3429,7 @@ packages: resolution: {integrity: sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==} dependencies: bser: 2.1.1 + dev: false /fetch-mock/9.11.0: resolution: {integrity: sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==} @@ -5857,6 +3493,7 @@ packages: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 + dev: false /flat-cache/3.0.4: resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} @@ -5936,6 +3573,7 @@ packages: /gensync/1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + dev: false /get-caller-file/2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} @@ -5952,10 +3590,12 @@ packages: /get-package-type/0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} + dev: false /get-stream/6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + dev: false /get-symbol-description/1.0.0: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} @@ -6007,6 +3647,16 @@ packages: once: 1.4.0 path-is-absolute: 1.0.1 + /glob/9.2.1: + resolution: {integrity: sha512-Pxxgq3W0HyA3XUvSXcFhRSs+43Jsx0ddxcFrbjxNGkL2Ak5BAUBxLqI5G6ADDeCHLfzzXFhe0b1yYcctGmytMA==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + fs.realpath: 1.0.0 + minimatch: 7.4.2 + minipass: 4.2.4 + path-scurry: 1.6.1 + dev: true + /globals/11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -6113,6 +3763,7 @@ packages: /html-escaper/2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: false /http-proxy-agent/5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} @@ -6138,17 +3789,7 @@ packages: /human-signals/2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - - /human-signals/3.0.1: - resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==} - engines: {node: '>=12.20.0'} - dev: true - - /husky/8.0.1: - resolution: {integrity: sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==} - engines: {node: '>=14'} - hasBin: true - dev: true + dev: false /iconv-lite/0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} @@ -6195,6 +3836,7 @@ packages: dependencies: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 + dev: false /imurmurhash/0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} @@ -6292,14 +3934,10 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - /is-fullwidth-code-point/4.0.0: - resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} - engines: {node: '>=12'} - dev: true - /is-generator-fn/2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} + dev: false /is-glob/4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} @@ -6347,11 +3985,7 @@ packages: /is-stream/2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - - /is-stream/3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true + dev: false /is-string/1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} @@ -6395,6 +4029,7 @@ packages: /istanbul-lib-coverage/3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} + dev: false /istanbul-lib-instrument/5.2.0: resolution: {integrity: sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==} @@ -6407,6 +4042,7 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color + dev: false /istanbul-lib-report/3.0.0: resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} @@ -6415,6 +4051,7 @@ packages: istanbul-lib-coverage: 3.2.0 make-dir: 3.1.0 supports-color: 7.2.0 + dev: false /istanbul-lib-source-maps/4.0.1: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} @@ -6425,6 +4062,7 @@ packages: source-map: 0.6.1 transitivePeerDependencies: - supports-color + dev: false /istanbul-reports/3.1.4: resolution: {integrity: sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==} @@ -6432,6 +4070,7 @@ packages: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.0 + dev: false /iterare/1.2.1: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} @@ -6449,41 +4088,44 @@ packages: minimatch: 3.1.2 dev: true - /jest-changed-files/29.0.0: - resolution: {integrity: sha512-28/iDMDrUpGoCitTURuDqUzWQoWmOmOKOFST1mi2lwh62X4BFf6khgH3uSuo1e49X/UDjuApAj3w0wLOex4VPQ==} + /jest-changed-files/29.5.0: + resolution: {integrity: sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: execa: 5.1.1 p-limit: 3.1.0 + dev: false - /jest-circus/29.0.3: - resolution: {integrity: sha512-QeGzagC6Hw5pP+df1+aoF8+FBSgkPmraC1UdkeunWh0jmrp7wC0Hr6umdUAOELBQmxtKAOMNC3KAdjmCds92Zg==} + /jest-circus/29.5.0: + resolution: {integrity: sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.0.3 - '@jest/expect': 29.0.3 - '@jest/test-result': 29.0.3 - '@jest/types': 29.0.3 + '@jest/environment': 29.5.0 + '@jest/expect': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 16.11.7 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 is-generator-fn: 2.1.0 - jest-each: 29.0.3 - jest-matcher-utils: 29.0.3 - jest-message-util: 29.0.3 - jest-runtime: 29.0.3 - jest-snapshot: 29.0.3 - jest-util: 29.0.3 + jest-each: 29.5.0 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-runtime: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 p-limit: 3.1.0 - pretty-format: 29.0.3 + pretty-format: 29.5.0 + pure-rand: 6.0.0 slash: 3.0.0 stack-utils: 2.0.5 transitivePeerDependencies: - supports-color + dev: false - /jest-cli/29.0.3_yqiaopbgmqcuvx27p5xxvum6wm: - resolution: {integrity: sha512-aUy9Gd/Kut1z80eBzG10jAn6BgS3BoBbXyv+uXEqBJ8wnnuZ5RpNfARoskSrTIy1GY4a8f32YGuCMwibtkl9CQ==} + /jest-cli/29.5.0_6m7kcbkkzjz4ln6z66tlzx44we: + resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -6492,25 +4134,26 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.0.3_ts-node@10.8.1 - '@jest/test-result': 29.0.3 - '@jest/types': 29.0.3 + '@jest/core': 29.5.0_ts-node@10.9.1 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.10 import-local: 3.1.0 - jest-config: 29.0.3_yqiaopbgmqcuvx27p5xxvum6wm - jest-util: 29.0.3 - jest-validate: 29.0.3 + jest-config: 29.5.0_6m7kcbkkzjz4ln6z66tlzx44we + jest-util: 29.5.0 + jest-validate: 29.5.0 prompts: 2.4.2 yargs: 17.5.1 transitivePeerDependencies: - '@types/node' - supports-color - ts-node + dev: false - /jest-config/29.0.3_yqiaopbgmqcuvx27p5xxvum6wm: - resolution: {integrity: sha512-U5qkc82HHVYe3fNu2CRXLN4g761Na26rWKf7CjM8LlZB3In1jadEkZdMwsE37rd9RSPV0NfYaCjHdk/gu3v+Ew==} + /jest-config/29.5.0_6m7kcbkkzjz4ln6z66tlzx44we: + resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@types/node': '*' @@ -6522,31 +4165,32 @@ packages: optional: true dependencies: '@babel/core': 7.18.9 - '@jest/test-sequencer': 29.0.3 - '@jest/types': 29.0.3 + '@jest/test-sequencer': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 16.11.7 - babel-jest: 29.0.3_@babel+core@7.18.9 + babel-jest: 29.5.0_@babel+core@7.18.9 chalk: 4.1.2 ci-info: 3.3.1 deepmerge: 4.2.2 glob: 7.2.0 graceful-fs: 4.2.10 - jest-circus: 29.0.3 - jest-environment-node: 29.0.3 - jest-get-type: 29.0.0 - jest-regex-util: 29.0.0 - jest-resolve: 29.0.3 - jest-runner: 29.0.3 - jest-util: 29.0.3 - jest-validate: 29.0.3 + jest-circus: 29.5.0 + jest-environment-node: 29.5.0 + jest-get-type: 29.4.3 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-runner: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.0.3 + pretty-format: 29.5.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.8.1_seagpw47opwyivxvtfydnuwcuy + ts-node: 10.9.1_p45lmeymo45ge573kyxcus632u transitivePeerDependencies: - supports-color + dev: false /jest-diff/29.0.3: resolution: {integrity: sha512-+X/AIF5G/vX9fWK+Db9bi9BQas7M9oBME7egU7psbn4jlszLFCu0dW63UgeE6cs/GANq4fLaT+8sGHQQ0eCUfg==} @@ -6554,83 +4198,106 @@ packages: dependencies: chalk: 4.1.2 diff-sequences: 29.0.0 - jest-get-type: 29.0.0 - pretty-format: 29.0.3 + jest-get-type: 29.4.3 + pretty-format: 29.5.0 - /jest-docblock/29.0.0: - resolution: {integrity: sha512-s5Kpra/kLzbqu9dEjov30kj1n4tfu3e7Pl8v+f8jOkeWNqM6Ds8jRaJfZow3ducoQUrf2Z4rs2N5S3zXnb83gw==} + /jest-diff/29.5.0: + resolution: {integrity: sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.4.3 + jest-get-type: 29.4.3 + pretty-format: 29.5.0 + dev: false + + /jest-docblock/29.4.3: + resolution: {integrity: sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: detect-newline: 3.1.0 + dev: false - /jest-each/29.0.3: - resolution: {integrity: sha512-wILhZfESURHHBNvPMJ0lZlYZrvOQJxAo3wNHi+ycr90V7M+uGR9Gh4+4a/BmaZF0XTyZsk4OiYEf3GJN7Ltqzg==} + /jest-each/29.5.0: + resolution: {integrity: sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.0.3 + '@jest/types': 29.5.0 chalk: 4.1.2 - jest-get-type: 29.0.0 - jest-util: 29.0.3 - pretty-format: 29.0.3 + jest-get-type: 29.4.3 + jest-util: 29.5.0 + pretty-format: 29.5.0 + dev: false - /jest-environment-jsdom/29.0.3: - resolution: {integrity: sha512-KIGvpm12c71hoYTjL4wC2c8K6KfhOHJqJtaHc1IApu5rG047YWZoEP13BlbucWfzGISBrmli8KFqdhdQEa8Wnw==} + /jest-environment-jsdom/29.5.0: + resolution: {integrity: sha512-/KG8yEK4aN8ak56yFVdqFDzKNHgF4BAymCx2LbPNPsUshUlfAl0eX402Xm1pt+eoG9SLZEUVifqXtX8SK74KCw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true dependencies: - '@jest/environment': 29.0.3 - '@jest/fake-timers': 29.0.3 - '@jest/types': 29.0.3 + '@jest/environment': 29.5.0 + '@jest/fake-timers': 29.5.0 + '@jest/types': 29.5.0 '@types/jsdom': 20.0.0 '@types/node': 16.11.7 - jest-mock: 29.0.3 - jest-util: 29.0.3 + jest-mock: 29.5.0 + jest-util: 29.5.0 jsdom: 20.0.0 transitivePeerDependencies: - bufferutil - - canvas - supports-color - utf-8-validate dev: true - /jest-environment-node/29.0.3: - resolution: {integrity: sha512-cdZqRCnmIlTXC+9vtvmfiY/40Cj6s2T0czXuq1whvQdmpzAnj4sbqVYuZ4zFHk766xTTJ+Ij3uUqkk8KCfXoyg==} + /jest-environment-node/29.5.0: + resolution: {integrity: sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.0.3 - '@jest/fake-timers': 29.0.3 - '@jest/types': 29.0.3 + '@jest/environment': 29.5.0 + '@jest/fake-timers': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 16.11.7 - jest-mock: 29.0.3 - jest-util: 29.0.3 + jest-mock: 29.5.0 + jest-util: 29.5.0 + dev: false /jest-get-type/29.0.0: resolution: {integrity: sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - /jest-haste-map/29.0.3: - resolution: {integrity: sha512-uMqR99+GuBHo0RjRhOE4iA6LmsxEwRdgiIAQgMU/wdT2XebsLDz5obIwLZm/Psj+GwSEQhw9AfAVKGYbh2G55A==} + /jest-get-type/29.4.3: + resolution: {integrity: sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /jest-haste-map/29.5.0: + resolution: {integrity: sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.0.3 + '@jest/types': 29.5.0 '@types/graceful-fs': 4.1.5 '@types/node': 16.11.7 anymatch: 3.1.2 fb-watchman: 2.0.1 graceful-fs: 4.2.10 - jest-regex-util: 29.0.0 - jest-util: 29.0.3 - jest-worker: 29.0.3 + jest-regex-util: 29.4.3 + jest-util: 29.5.0 + jest-worker: 29.5.0 micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: fsevents: 2.3.2 + dev: false - /jest-leak-detector/29.0.3: - resolution: {integrity: sha512-YfW/G63dAuiuQ3QmQlh8hnqLDe25WFY3eQhuc/Ev1AGmkw5zREblTh7TCSKLoheyggu6G9gxO2hY8p9o6xbaRQ==} + /jest-leak-detector/29.5.0: + resolution: {integrity: sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-get-type: 29.0.0 - pretty-format: 29.0.3 + jest-get-type: 29.4.3 + pretty-format: 29.5.0 + dev: false /jest-matcher-utils/29.0.3: resolution: {integrity: sha512-RsR1+cZ6p1hDV4GSCQTg+9qjeotQCgkaleIKLK7dm+U4V/H2bWedU3RAtLm8+mANzZ7eDV33dMar4pejd7047w==} @@ -6639,21 +4306,16 @@ packages: chalk: 4.1.2 jest-diff: 29.0.3 jest-get-type: 29.0.0 - pretty-format: 29.0.3 + pretty-format: 29.5.0 - /jest-message-util/28.1.1: - resolution: {integrity: sha512-xoDOOT66fLfmTRiqkoLIU7v42mal/SqwDKvfmfiWAdJMSJiU+ozgluO7KbvoAgiwIrrGZsV7viETjc8GNrA/IQ==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-matcher-utils/29.5.0: + resolution: {integrity: sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/code-frame': 7.18.6 - '@jest/types': 28.1.1 - '@types/stack-utils': 2.0.1 chalk: 4.1.2 - graceful-fs: 4.2.10 - micromatch: 4.0.5 - pretty-format: 28.1.1 - slash: 3.0.0 - stack-utils: 2.0.5 + jest-diff: 29.5.0 + jest-get-type: 29.4.3 + pretty-format: 29.5.0 dev: false /jest-message-util/29.0.3: @@ -6661,23 +4323,38 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/code-frame': 7.18.6 - '@jest/types': 29.0.3 + '@jest/types': 29.5.0 '@types/stack-utils': 2.0.1 chalk: 4.1.2 graceful-fs: 4.2.10 micromatch: 4.0.5 - pretty-format: 29.0.3 + pretty-format: 29.5.0 slash: 3.0.0 stack-utils: 2.0.5 - /jest-mock/29.0.3: - resolution: {integrity: sha512-ort9pYowltbcrCVR43wdlqfAiFJXBx8l4uJDsD8U72LgBcetvEp+Qxj1W9ZYgMRoeAo+ov5cnAGF2B6+Oth+ww==} + /jest-message-util/29.5.0: + resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.0.3 - '@types/node': 16.11.7 + '@babel/code-frame': 7.18.6 + '@jest/types': 29.5.0 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.10 + micromatch: 4.0.5 + pretty-format: 29.5.0 + slash: 3.0.0 + stack-utils: 2.0.5 - /jest-pnp-resolver/1.2.2_jest-resolve@29.0.3: + /jest-mock/29.5.0: + resolution: {integrity: sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.5.0 + '@types/node': 16.11.7 + jest-util: 29.5.0 + + /jest-pnp-resolver/1.2.2_jest-resolve@29.5.0: resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==} engines: {node: '>=6'} peerDependencies: @@ -6686,99 +4363,105 @@ packages: jest-resolve: optional: true dependencies: - jest-resolve: 29.0.3 - - /jest-regex-util/28.0.2: - resolution: {integrity: sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + jest-resolve: 29.5.0 dev: false /jest-regex-util/29.0.0: resolution: {integrity: sha512-BV7VW7Sy0fInHWN93MMPtlClweYv2qrSCwfeFWmpribGZtQPWNvRSq9XOVgOEjU1iBGRKXUZil0o2AH7Iy9Lug==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false - /jest-resolve-dependencies/29.0.3: - resolution: {integrity: sha512-KzuBnXqNvbuCdoJpv8EanbIGObk7vUBNt/PwQPPx2aMhlv/jaXpUJsqWYRpP/0a50faMBY7WFFP8S3/CCzwfDw==} + /jest-regex-util/29.4.3: + resolution: {integrity: sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false + + /jest-resolve-dependencies/29.5.0: + resolution: {integrity: sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-regex-util: 29.0.0 - jest-snapshot: 29.0.3 + jest-regex-util: 29.4.3 + jest-snapshot: 29.5.0 transitivePeerDependencies: - supports-color + dev: false - /jest-resolve/29.0.3: - resolution: {integrity: sha512-toVkia85Y/BPAjJasTC9zIPY6MmVXQPtrCk8SmiheC4MwVFE/CMFlOtMN6jrwPMC6TtNh8+sTMllasFeu1wMPg==} + /jest-resolve/29.5.0: + resolution: {integrity: sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 graceful-fs: 4.2.10 - jest-haste-map: 29.0.3 - jest-pnp-resolver: 1.2.2_jest-resolve@29.0.3 - jest-util: 29.0.3 - jest-validate: 29.0.3 + jest-haste-map: 29.5.0 + jest-pnp-resolver: 1.2.2_jest-resolve@29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 resolve: 1.22.1 - resolve.exports: 1.1.0 + resolve.exports: 2.0.1 slash: 3.0.0 + dev: false - /jest-runner/29.0.3: - resolution: {integrity: sha512-Usu6VlTOZlCZoNuh3b2Tv/yzDpKqtiNAetG9t3kJuHfUyVMNW7ipCCJOUojzKkjPoaN7Bl1f7Buu6PE0sGpQxw==} + /jest-runner/29.5.0: + resolution: {integrity: sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.0.3 - '@jest/environment': 29.0.3 - '@jest/test-result': 29.0.3 - '@jest/transform': 29.0.3 - '@jest/types': 29.0.3 + '@jest/console': 29.5.0 + '@jest/environment': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 16.11.7 chalk: 4.1.2 - emittery: 0.10.2 + emittery: 0.13.1 graceful-fs: 4.2.10 - jest-docblock: 29.0.0 - jest-environment-node: 29.0.3 - jest-haste-map: 29.0.3 - jest-leak-detector: 29.0.3 - jest-message-util: 29.0.3 - jest-resolve: 29.0.3 - jest-runtime: 29.0.3 - jest-util: 29.0.3 - jest-watcher: 29.0.3 - jest-worker: 29.0.3 + jest-docblock: 29.4.3 + jest-environment-node: 29.5.0 + jest-haste-map: 29.5.0 + jest-leak-detector: 29.5.0 + jest-message-util: 29.5.0 + jest-resolve: 29.5.0 + jest-runtime: 29.5.0 + jest-util: 29.5.0 + jest-watcher: 29.5.0 + jest-worker: 29.5.0 p-limit: 3.1.0 source-map-support: 0.5.13 transitivePeerDependencies: - supports-color + dev: false - /jest-runtime/29.0.3: - resolution: {integrity: sha512-12gZXRQ7ozEeEHKTY45a+YLqzNDR/x4c//X6AqwKwKJPpWM8FY4vwn4VQJOcLRS3Nd1fWwgP7LU4SoynhuUMHQ==} + /jest-runtime/29.5.0: + resolution: {integrity: sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.0.3 - '@jest/fake-timers': 29.0.3 - '@jest/globals': 29.0.3 - '@jest/source-map': 29.0.0 - '@jest/test-result': 29.0.3 - '@jest/transform': 29.0.3 - '@jest/types': 29.0.3 + '@jest/environment': 29.5.0 + '@jest/fake-timers': 29.5.0 + '@jest/globals': 29.5.0 + '@jest/source-map': 29.4.3 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 16.11.7 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 glob: 7.2.0 graceful-fs: 4.2.10 - jest-haste-map: 29.0.3 - jest-message-util: 29.0.3 - jest-mock: 29.0.3 - jest-regex-util: 29.0.0 - jest-resolve: 29.0.3 - jest-snapshot: 29.0.3 - jest-util: 29.0.3 + jest-haste-map: 29.5.0 + jest-message-util: 29.5.0 + jest-mock: 29.5.0 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color + dev: false - /jest-snapshot/29.0.3: - resolution: {integrity: sha512-52q6JChm04U3deq+mkQ7R/7uy7YyfVIrebMi6ZkBoDJ85yEjm/sJwdr1P0LOIEHmpyLlXrxy3QP0Zf5J2kj0ew==} + /jest-snapshot/29.5.0: + resolution: {integrity: sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.18.9 @@ -6787,26 +4470,26 @@ packages: '@babel/plugin-syntax-typescript': 7.17.12_@babel+core@7.18.9 '@babel/traverse': 7.18.9 '@babel/types': 7.18.9 - '@jest/expect-utils': 29.0.3 - '@jest/transform': 29.0.3 - '@jest/types': 29.0.3 + '@jest/expect-utils': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/babel__traverse': 7.17.1 '@types/prettier': 2.6.3 babel-preset-current-node-syntax: 1.0.1_@babel+core@7.18.9 chalk: 4.1.2 - expect: 29.0.3 + expect: 29.5.0 graceful-fs: 4.2.10 - jest-diff: 29.0.3 - jest-get-type: 29.0.0 - jest-haste-map: 29.0.3 - jest-matcher-utils: 29.0.3 - jest-message-util: 29.0.3 - jest-util: 29.0.3 + jest-diff: 29.5.0 + jest-get-type: 29.4.3 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-util: 29.5.0 natural-compare: 1.4.0 - pretty-format: 29.0.3 + pretty-format: 29.5.0 semver: 7.3.7 transitivePeerDependencies: - supports-color + dev: false /jest-sonar-reporter/2.0.0: resolution: {integrity: sha512-ZervDCgEX5gdUbdtWsjdipLN3bKJwpxbvhkYNXTAYvAckCihobSLr9OT/IuyNIRT1EZMDDwR6DroWtrq+IL64w==} @@ -6815,78 +4498,64 @@ packages: xml: 1.0.1 dev: true - /jest-styled-components/7.0.8_styled-components@5.3.1: - resolution: {integrity: sha512-0KE54d0yIzKcvtOv8eikyjG3rFRtKYUyQovaoha3nondtZzXYGB3bhsvYgEegU08Iry0ndWx2+g9f5ZzD4I+0Q==} + /jest-styled-components/7.1.1_styled-components@5.3.1: + resolution: {integrity: sha512-OUq31R5CivBF8oy81dnegNQrRW13TugMol/Dz6ZnFfEyo03exLASod7YGwyHGuayYlKmCstPtz0RQ1+NrAbIIA==} engines: {node: '>= 12'} peerDependencies: styled-components: '>= 5' dependencies: - css: 3.0.0 + '@adobe/css-tools': 4.2.0 styled-components: 5.3.1_uuaz5p7xzfmtjacf6iqf7idnby dev: true - /jest-util/28.1.1: - resolution: {integrity: sha512-FktOu7ca1DZSyhPAxgxB6hfh2+9zMoJ7aEQA759Z6p45NuO8mWcqujH+UdHlCm/V6JTWwDztM2ITCzU1ijJAfw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@jest/types': 28.1.1 - '@types/node': 16.11.7 - chalk: 4.1.2 - ci-info: 3.3.1 - graceful-fs: 4.2.10 - picomatch: 2.3.1 - dev: false - /jest-util/29.0.3: resolution: {integrity: sha512-Q0xaG3YRG8QiTC4R6fHjHQPaPpz9pJBEi0AeOE4mQh/FuWOijFjGXMMOfQEaU9i3z76cNR7FobZZUQnL6IyfdQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.0.3 + '@jest/types': 29.5.0 '@types/node': 16.11.7 chalk: 4.1.2 ci-info: 3.3.1 graceful-fs: 4.2.10 picomatch: 2.3.1 - /jest-validate/29.0.3: - resolution: {integrity: sha512-OebiqqT6lK8cbMPtrSoS3aZP4juID762lZvpf1u+smZnwTEBCBInan0GAIIhv36MxGaJvmq5uJm7dl5gVt+Zrw==} + /jest-util/29.5.0: + resolution: {integrity: sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.0.3 + '@jest/types': 29.5.0 + '@types/node': 16.11.7 + chalk: 4.1.2 + ci-info: 3.3.1 + graceful-fs: 4.2.10 + picomatch: 2.3.1 + + /jest-validate/29.5.0: + resolution: {integrity: sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.5.0 camelcase: 6.3.0 chalk: 4.1.2 - jest-get-type: 29.0.0 + jest-get-type: 29.4.3 leven: 3.1.0 - pretty-format: 29.0.3 - - /jest-watch-typeahead/2.0.0_jest@29.0.3: - resolution: {integrity: sha512-LX/jrTNkOIZV1n7fnEWlKJ73lhVPKF9B6F0L7pbje3xNPw1NddPLn0n8EFt7YwbCnC0+vlLylp7JehV6M06J6Q==} - engines: {node: ^14.17.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - jest: ^27.0.0 || ^28.0.0 - dependencies: - ansi-escapes: 5.0.0 - chalk: 4.1.2 - jest: 29.0.3_yqiaopbgmqcuvx27p5xxvum6wm - jest-regex-util: 28.0.2 - jest-watcher: 28.1.1 - slash: 4.0.0 - string-length: 5.0.1 - strip-ansi: 7.0.1 + pretty-format: 29.5.0 dev: false - /jest-watcher/28.1.1: - resolution: {integrity: sha512-RQIpeZ8EIJMxbQrXpJQYIIlubBnB9imEHsxxE41f54ZwcqWLysL/A0ZcdMirf+XsMn3xfphVQVV4EW0/p7i7Ug==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-watch-typeahead/2.2.2_jest@29.5.0: + resolution: {integrity: sha512-+QgOFW4o5Xlgd6jGS5X37i08tuuXNW8X0CV9WNFi+3n8ExCIP+E1melYhvYLjv5fE6D0yyzk74vsSO8I6GqtvQ==} + engines: {node: ^14.17.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + jest: ^27.0.0 || ^28.0.0 || ^29.0.0 dependencies: - '@jest/test-result': 28.1.1 - '@jest/types': 28.1.1 - '@types/node': 16.11.7 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.10.2 - jest-util: 28.1.1 - string-length: 4.0.2 + ansi-escapes: 6.0.0 + chalk: 5.2.0 + jest: 29.5.0_6m7kcbkkzjz4ln6z66tlzx44we + jest-regex-util: 29.0.0 + jest-watcher: 29.0.3 + slash: 5.0.0 + string-length: 5.0.1 + strip-ansi: 7.0.1 dev: false /jest-watcher/29.0.3: @@ -6894,24 +4563,41 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/test-result': 29.0.3 - '@jest/types': 29.0.3 + '@jest/types': 29.5.0 '@types/node': 16.11.7 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.10.2 - jest-util: 29.0.3 + jest-util: 29.5.0 string-length: 4.0.2 + dev: false - /jest-worker/29.0.3: - resolution: {integrity: sha512-Tl/YWUugQOjoTYwjKdfJWkSOfhufJHO5LhXTSZC3TRoQKO+fuXnZAdoXXBlpLXKGODBL3OvdUasfDD4PcMe6ng==} + /jest-watcher/29.5.0: + resolution: {integrity: sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 16.11.7 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.5.0 + string-length: 4.0.2 + dev: false + + /jest-worker/29.5.0: + resolution: {integrity: sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@types/node': 16.11.7 + jest-util: 29.5.0 merge-stream: 2.0.0 supports-color: 8.1.1 + dev: false - /jest/29.0.3_yqiaopbgmqcuvx27p5xxvum6wm: - resolution: {integrity: sha512-ElgUtJBLgXM1E8L6K1RW1T96R897YY/3lRYqq9uVcPWtP2AAl/nQ16IYDh/FzQOOQ12VEuLdcPU83mbhG2C3PQ==} + /jest/29.5.0_6m7kcbkkzjz4ln6z66tlzx44we: + resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -6920,14 +4606,15 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.0.3_ts-node@10.8.1 - '@jest/types': 29.0.3 + '@jest/core': 29.5.0_ts-node@10.9.1 + '@jest/types': 29.5.0 import-local: 3.1.0 - jest-cli: 29.0.3_yqiaopbgmqcuvx27p5xxvum6wm + jest-cli: 29.5.0_6m7kcbkkzjz4ln6z66tlzx44we transitivePeerDependencies: - '@types/node' - supports-color - ts-node + dev: false /js-tokens/4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -6938,6 +4625,7 @@ packages: dependencies: argparse: 1.0.10 esprima: 4.0.1 + dev: false /js-yaml/4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} @@ -6988,11 +4676,6 @@ packages: - utf-8-validate dev: true - /jsesc/0.5.0: - resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} - hasBin: true - dev: true - /jsesc/2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -7058,6 +4741,11 @@ packages: engines: {node: '>=10.0.0'} dev: false + /jsonpath-plus/7.2.0: + resolution: {integrity: sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==} + engines: {node: '>=12.0.0'} + dev: false + /jsx-ast-utils/3.3.0: resolution: {integrity: sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==} engines: {node: '>=4.0'} @@ -7069,6 +4757,7 @@ packages: /kleur/3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + dev: false /language-subtag-registry/0.3.21: resolution: {integrity: sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==} @@ -7083,6 +4772,7 @@ packages: /leven/3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + dev: false /levn/0.3.0: resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} @@ -7100,56 +4790,9 @@ packages: type-check: 0.4.0 dev: true - /lilconfig/2.0.5: - resolution: {integrity: sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==} - engines: {node: '>=10'} - dev: true - /lines-and-columns/1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - /lint-staged/13.0.2: - resolution: {integrity: sha512-qQLfLTh9z34eMzfEHENC+QBskZfxjomrf+snF3xJ4BzilORbD989NLqQ00ughsF/A+PT41e87+WsMFabf9++pQ==} - engines: {node: ^14.13.1 || >=16.0.0} - hasBin: true - dependencies: - cli-truncate: 3.1.0 - colorette: 2.0.19 - commander: 9.3.0 - debug: 4.3.4 - execa: 6.1.0 - lilconfig: 2.0.5 - listr2: 4.0.5 - micromatch: 4.0.5 - normalize-path: 3.0.0 - object-inspect: 1.12.2 - pidtree: 0.6.0 - string-argv: 0.3.1 - yaml: 2.1.1 - transitivePeerDependencies: - - enquirer - - supports-color - dev: true - - /listr2/4.0.5: - resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} - engines: {node: '>=12'} - peerDependencies: - enquirer: '>= 2.3.0 < 3' - peerDependenciesMeta: - enquirer: - optional: true - dependencies: - cli-truncate: 2.1.0 - colorette: 2.0.19 - log-update: 4.0.0 - p-map: 4.0.0 - rfdc: 1.3.0 - rxjs: 7.5.5 - through: 2.3.8 - wrap-ansi: 7.0.0 - dev: true - /locate-path/2.0.0: resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} engines: {node: '>=4'} @@ -7163,15 +4806,8 @@ packages: engines: {node: '>=8'} dependencies: p-locate: 4.1.0 - - /lodash-es/4.17.21: - resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} dev: false - /lodash.debounce/4.0.8: - resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - dev: true - /lodash.get/4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} dev: false @@ -7199,16 +4835,6 @@ packages: is-unicode-supported: 0.1.0 dev: true - /log-update/4.0.0: - resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} - engines: {node: '>=10'} - dependencies: - ansi-escapes: 4.3.2 - cli-cursor: 3.1.0 - slice-ansi: 4.0.0 - wrap-ansi: 6.2.0 - dev: true - /loose-envify/1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -7221,6 +4847,11 @@ packages: dependencies: yallist: 4.0.0 + /lru-cache/7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + dev: true + /lz-string/1.4.4: resolution: {integrity: sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==} hasBin: true @@ -7230,6 +4861,7 @@ packages: engines: {node: '>=8'} dependencies: semver: 6.3.0 + dev: false /make-error/1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -7238,9 +4870,11 @@ packages: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} dependencies: tmpl: 1.0.5 + dev: false /merge-stream/2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: false /merge2/1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} @@ -7270,11 +4904,6 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - /mimic-fn/4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - dev: true - /min-indent/1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -7292,10 +4921,22 @@ packages: brace-expansion: 2.0.1 dev: true + /minimatch/7.4.2: + resolution: {integrity: sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist/1.2.6: resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} dev: true + /minipass/4.2.4: + resolution: {integrity: sha512-lwycX3cBMTvcejsHITUgYj6Gy6A7Nh4Q6h9NP4sTHY1ccJlC7yKzDmiShEHsJ16Jf1nKGDEaiHxiltsJEvk0nQ==} + engines: {node: '>=8'} + dev: true + /mkdirp/1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -7317,10 +4958,6 @@ packages: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} dev: true - /nanoclone/0.2.1: - resolution: {integrity: sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==} - dev: false - /nanoid/3.3.4: resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -7343,9 +4980,11 @@ packages: /node-int64/0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + dev: false /node-releases/2.0.5: resolution: {integrity: sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==} + dev: false /normalize-path/3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -7356,13 +4995,7 @@ packages: engines: {node: '>=8'} dependencies: path-key: 3.1.1 - - /npm-run-path/5.1.0: - resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - path-key: 4.0.0 - dev: true + dev: false /nwsapi/2.2.0: resolution: {integrity: sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==} @@ -7441,13 +5074,6 @@ packages: dependencies: mimic-fn: 2.1.0 - /onetime/6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - dependencies: - mimic-fn: 4.0.0 - dev: true - /ono/4.0.11: resolution: {integrity: sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==} dependencies: @@ -7519,12 +5145,14 @@ packages: engines: {node: '>=6'} dependencies: p-try: 2.2.0 + dev: false /p-limit/3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} dependencies: yocto-queue: 0.1.0 + dev: false /p-locate/2.0.0: resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} @@ -7538,13 +5166,7 @@ packages: engines: {node: '>=8'} dependencies: p-limit: 2.3.0 - - /p-map/4.0.0: - resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} - engines: {node: '>=10'} - dependencies: - aggregate-error: 3.1.0 - dev: true + dev: false /p-try/1.0.0: resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} @@ -7554,6 +5176,7 @@ packages: /p-try/2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + dev: false /parent-module/1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -7594,6 +5217,7 @@ packages: /path-exists/4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + dev: false /path-is-absolute/1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} @@ -7603,14 +5227,17 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - /path-key/4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - dev: true - /path-parse/1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + /path-scurry/1.6.1: + resolution: {integrity: sha512-OW+5s+7cw6253Q4E+8qQ/u1fVvcJQCJo/VFD8pje+dbJCF1n5ZRMV2AEHbGp+5Q7jxQIYJxkHopnj6nzdGeZLA==} + engines: {node: '>=14'} + dependencies: + lru-cache: 7.18.3 + minipass: 4.2.4 + dev: true + /path-to-regexp/2.4.0: resolution: {integrity: sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==} dev: false @@ -7631,21 +5258,17 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - /pidtree/0.6.0: - resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} - engines: {node: '>=0.10'} - hasBin: true - dev: true - /pirates/4.0.5: resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} engines: {node: '>= 6'} + dev: false /pkg-dir/4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} dependencies: find-up: 4.1.0 + dev: false /postcss-value-parser/4.1.0: resolution: {integrity: sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==} @@ -7675,8 +5298,8 @@ packages: fast-diff: 1.2.0 dev: true - /prettier/2.5.1: - resolution: {integrity: sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==} + /prettier/2.8.4: + resolution: {integrity: sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==} engines: {node: '>=10.13.0'} hasBin: true dev: true @@ -7689,16 +5312,6 @@ packages: ansi-styles: 5.2.0 react-is: 17.0.2 - /pretty-format/28.1.1: - resolution: {integrity: sha512-wwJbVTGFHeucr5Jw2bQ9P+VYHyLdAqedFLEkdQUVaBF/eiidDwH5OpilINq4mEfhbCjLnirt6HTTDhv1HaTIQw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@jest/schemas': 28.0.2 - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 18.2.0 - dev: false - /pretty-format/29.0.3: resolution: {integrity: sha512-cHudsvQr1K5vNVLbvYF/nv3Qy/F/BcEKxGuIeMiVMRHxPOO1RxXooP8g/ZrwAp7Dx+KdMZoOc7NxLHhMrP2f9Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7707,6 +5320,14 @@ packages: ansi-styles: 5.2.0 react-is: 18.2.0 + /pretty-format/29.5.0: + resolution: {integrity: sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.4.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + /pretty-ms/7.0.1: resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} engines: {node: '>=10'} @@ -7720,6 +5341,7 @@ packages: dependencies: kleur: 3.0.3 sisteransi: 1.0.5 + dev: false /prop-types/15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -7728,8 +5350,8 @@ packages: object-assign: 4.1.1 react-is: 16.13.1 - /property-expr/2.0.4: - resolution: {integrity: sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==} + /property-expr/2.0.5: + resolution: {integrity: sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==} dev: false /psl/1.8.0: @@ -7740,6 +5362,10 @@ packages: resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} engines: {node: '>=6'} + /pure-rand/6.0.0: + resolution: {integrity: sha512-rLSBxJjP+4DQOgcJAx6RZHT2he2pkhQdSnofG5VWyVl6GRq/K02ISOuOLcsMOrtKDIJb8JN2zm3FFzWNbezdPw==} + dev: false + /querystring/0.2.1: resolution: {integrity: sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==} engines: {node: '>=0.4.x'} @@ -7765,8 +5391,8 @@ packages: react-dom: 18.1.0_react@18.1.0 dev: false - /react-datepicker/4.8.0_ef5jwxihqo6n7gxfmzogljlgcm: - resolution: {integrity: sha512-u69zXGHMpxAa4LeYR83vucQoUCJQ6m/WBsSxmUMu/M8ahTSVMMyiyQzauHgZA2NUr9y0FUgOAix71hGYUb6tvg==} + /react-datepicker/4.10.0_ef5jwxihqo6n7gxfmzogljlgcm: + resolution: {integrity: sha512-6IfBCZyWj54ZZGLmEZJ9c4Yph0s9MVfEGDC2evOvf9AmVz+RRcfP2Czqad88Ff9wREbcbqa4dk7IFYeXF1d3Ag==} peerDependencies: react: ^16.9.0 || ^17 || ^18 react-dom: ^16.9.0 || ^17 || ^18 @@ -7777,8 +5403,8 @@ packages: prop-types: 15.8.1 react: 18.1.0 react-dom: 18.1.0_react@18.1.0 - react-onclickoutside: 6.12.1_ef5jwxihqo6n7gxfmzogljlgcm - react-popper: 2.2.5_kse2oiarfsejvukvxwgibirnca + react-onclickoutside: 6.12.2_ef5jwxihqo6n7gxfmzogljlgcm + react-popper: 2.3.0_7hirfqu277ous3575tvkbri6ie dev: false /react-dom/18.1.0_react@18.1.0: @@ -7803,16 +5429,17 @@ packages: /react-fast-compare/3.2.0: resolution: {integrity: sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==} - /react-hook-form/7.6.9_react@18.1.0: - resolution: {integrity: sha512-nz+btC4WFIm3zPBjw22K3t9nnJtlMMwj8slcbPYoTKlkSVA5l+q3Ai+VF0YzeRi7vbyyeGQvpyibov1xd/TV7A==} + /react-hook-form/7.43.1_react@18.1.0: + resolution: {integrity: sha512-+s3+s8LLytRMriwwuSqeLStVjRXFGxgjjx2jED7Z+wz1J/88vpxieRQGvJVvzrzVxshZ0BRuocFERb779m2kNg==} + engines: {node: '>=12.22.0'} peerDependencies: - react: ^16.8.0 || ^17 + react: ^16.8.0 || ^17 || ^18 dependencies: react: 18.1.0 dev: false - /react-hot-toast/2.3.0_ef5jwxihqo6n7gxfmzogljlgcm: - resolution: {integrity: sha512-/RxV+bfjld7tSJR1SCLzMAXgFuNW7fCpK6+vbYqfmbGSWcqTMz2rizrvfWKvtcPH5HK0NqxmBaC5SrAy1F42zA==} + /react-hot-toast/2.4.0_ef5jwxihqo6n7gxfmzogljlgcm: + resolution: {integrity: sha512-qnnVbXropKuwUpriVVosgo8QrB+IaPJCpL8oBI6Ov84uvHZ5QQcTp2qg6ku2wNfgJl6rlQXJIQU5q+5lmPOutA==} engines: {node: '>=10'} peerDependencies: react: '>=16' @@ -7844,11 +5471,11 @@ packages: react-dom: 18.1.0_react@18.1.0 dev: false - /react-onclickoutside/6.12.1_ef5jwxihqo6n7gxfmzogljlgcm: - resolution: {integrity: sha512-a5Q7CkWznBRUWPmocCvE8b6lEYw1s6+opp/60dCunhO+G6E4tDTO2Sd2jKE+leEnnrLAE2Wj5DlDHNqj5wPv1Q==} + /react-onclickoutside/6.12.2_ef5jwxihqo6n7gxfmzogljlgcm: + resolution: {integrity: sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA==} peerDependencies: - react: ^15.5.x || ^16.x || ^17.x - react-dom: ^15.5.x || ^16.x || ^17.x + react: ^15.5.x || ^16.x || ^17.x || ^18.x + react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x dependencies: react: 18.1.0 react-dom: 18.1.0_react@18.1.0 @@ -7864,6 +5491,21 @@ packages: react: 18.1.0 react-fast-compare: 3.2.0 warning: 4.0.3 + dev: true + + /react-popper/2.3.0_7hirfqu277ous3575tvkbri6ie: + resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.9.2 + react: 18.1.0 + react-dom: 18.1.0_react@18.1.0 + react-fast-compare: 3.2.0 + warning: 4.0.3 + dev: false /react-redux/8.0.2_nfqigfgwurfoimtkde74cji6ga: resolution: {integrity: sha512-nBwiscMw3NoP59NFCXFf02f8xdo+vSHT/uZ1ldDwF7XaTpzm+Phk97VT4urYBl5TYAPNVaFm12UHAEyzkpNzRA==} @@ -7977,26 +5619,9 @@ packages: resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} dev: true - /regenerate-unicode-properties/10.0.1: - resolution: {integrity: sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==} - engines: {node: '>=4'} - dependencies: - regenerate: 1.4.2 - dev: true - - /regenerate/1.4.2: - resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - dev: true - /regenerator-runtime/0.13.7: resolution: {integrity: sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==} - /regenerator-transform/0.15.0: - resolution: {integrity: sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==} - dependencies: - '@babel/runtime': 7.17.9 - dev: true - /regexp.prototype.flags/1.4.3: resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} engines: {node: '>= 0.4'} @@ -8011,29 +5636,6 @@ packages: engines: {node: '>=8'} dev: true - /regexpu-core/5.0.1: - resolution: {integrity: sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw==} - engines: {node: '>=4'} - dependencies: - regenerate: 1.4.2 - regenerate-unicode-properties: 10.0.1 - regjsgen: 0.6.0 - regjsparser: 0.8.4 - unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.0.0 - dev: true - - /regjsgen/0.6.0: - resolution: {integrity: sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==} - dev: true - - /regjsparser/0.8.4: - resolution: {integrity: sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA==} - hasBin: true - dependencies: - jsesc: 0.5.0 - dev: true - /require-directory/2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -8057,6 +5659,7 @@ packages: engines: {node: '>=8'} dependencies: resolve-from: 5.0.0 + dev: false /resolve-from/4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} @@ -8066,10 +5669,12 @@ packages: /resolve-from/5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + dev: false - /resolve.exports/1.1.0: - resolution: {integrity: sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==} + /resolve.exports/2.0.1: + resolution: {integrity: sha512-OEJWVeimw8mgQuj3HfkNl4KqRevH7lzeQNaWRPfx0PPse7Jk6ozcsG4FKVgtzDsC1KUF+YlTHh17NcgHOPykLw==} engines: {node: '>=10'} + dev: false /resolve/1.22.0: resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==} @@ -8108,10 +5713,6 @@ packages: engines: {iojs: '>=1.0.0', node: '>=0.10.0'} dev: true - /rfdc/1.3.0: - resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} - dev: true - /rimraf/3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -8119,6 +5720,14 @@ packages: glob: 7.2.0 dev: true + /rimraf/4.3.1: + resolution: {integrity: sha512-GfHJHBzFQra23IxDzIdBqhOWfbtdgS1/dCHrDy+yvhpoJY5TdwdT28oWaHWfRpKFDLd3GZnGTx6Mlt4+anbsxQ==} + engines: {node: '>=14'} + hasBin: true + dependencies: + glob: 9.2.1 + dev: true + /rollup/3.7.3: resolution: {integrity: sha512-7e68MQbAWCX6mI4/0lG1WHd+NdNAlVamg0Zkd+8LZ/oXojligdGnCNyHlzXqXCZObyjs5FRc3AH0b17iJESGIQ==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -8147,11 +5756,12 @@ packages: /rxjs/7.5.5: resolution: {integrity: sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==} dependencies: - tslib: 2.3.1 + tslib: 2.4.0 dev: true /safe-buffer/5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: false /safe-buffer/5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -8186,11 +5796,6 @@ packages: resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} hasBin: true - /semver/7.0.0: - resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==} - hasBin: true - dev: true - /semver/7.3.7: resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==} engines: {node: '>=10'} @@ -8224,6 +5829,7 @@ packages: /sisteransi/1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: false /slash/3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} @@ -8232,50 +5838,23 @@ packages: /slash/4.0.0: resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} engines: {node: '>=12'} - - /slice-ansi/3.0.0: - resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} - engines: {node: '>=8'} - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 dev: true - /slice-ansi/4.0.0: - resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - dev: true - - /slice-ansi/5.0.0: - resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} - engines: {node: '>=12'} - dependencies: - ansi-styles: 6.1.0 - is-fullwidth-code-point: 4.0.0 - dev: true + /slash/5.0.0: + resolution: {integrity: sha512-n6KkmvKS0623igEVj3FF0OZs1gYYJ0o0Hj939yc1fyxl2xt+xYpLnzJB6xBSqOfV9ZFLEWodBBN/heZJahuIJQ==} + engines: {node: '>=14.16'} + dev: false /source-map-js/1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} - /source-map-resolve/0.6.0: - resolution: {integrity: sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==} - deprecated: See https://github.com/lydell/source-map-resolve#deprecated - dependencies: - atob: 2.1.2 - decode-uri-component: 0.2.0 - dev: true - /source-map-support/0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} dependencies: buffer-from: 1.1.2 source-map: 0.6.1 + dev: false /source-map/0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} @@ -8287,6 +5866,7 @@ packages: /sprintf-js/1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: false /stack-utils/2.0.5: resolution: {integrity: sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==} @@ -8294,17 +5874,13 @@ packages: dependencies: escape-string-regexp: 2.0.0 - /string-argv/0.3.1: - resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} - engines: {node: '>=0.6.19'} - dev: true - /string-length/4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} dependencies: char-regex: 1.0.2 strip-ansi: 6.0.1 + dev: false /string-length/5.0.1: resolution: {integrity: sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==} @@ -8314,10 +5890,6 @@ packages: strip-ansi: 7.0.1 dev: false - /string-natural-compare/3.0.1: - resolution: {integrity: sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==} - dev: true - /string-width/4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -8326,15 +5898,6 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - /string-width/5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.0.1 - dev: true - /string.prototype.matchall/4.0.7: resolution: {integrity: sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==} dependencies: @@ -8381,6 +5944,7 @@ packages: engines: {node: '>=12'} dependencies: ansi-regex: 6.0.1 + dev: false /strip-bom/3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} @@ -8390,15 +5954,12 @@ packages: /strip-bom/4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} + dev: false /strip-final-newline/2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - - /strip-final-newline/3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - dev: true + dev: false /strip-indent/3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} @@ -8451,13 +6012,6 @@ packages: dependencies: has-flag: 4.0.0 - /supports-hyperlinks/2.2.0: - resolution: {integrity: sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - supports-color: 7.2.0 - /supports-preserve-symlinks-flag/1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -8474,18 +6028,15 @@ packages: tslib: 2.4.0 dev: true + /tabbable/6.1.1: + resolution: {integrity: sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg==} + dev: false + /tapable/2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} dev: true - /terminal-link/2.1.1: - resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} - engines: {node: '>=8'} - dependencies: - ansi-escapes: 4.3.2 - supports-hyperlinks: 2.2.0 - /test-exclude/6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -8493,6 +6044,7 @@ packages: '@istanbuljs/schema': 0.1.3 glob: 7.2.0 minimatch: 3.1.2 + dev: false /text-table/0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -8502,6 +6054,10 @@ packages: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: true + /tiny-case/1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + dev: false + /tiny-glob/0.2.9: resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} dependencies: @@ -8518,6 +6074,7 @@ packages: /tmpl/1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + dev: false /to-fast-properties/2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} @@ -8576,8 +6133,8 @@ packages: code-block-writer: 11.0.1 dev: true - /ts-node/10.8.1_seagpw47opwyivxvtfydnuwcuy: - resolution: {integrity: sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g==} + /ts-node/10.9.1_p45lmeymo45ge573kyxcus632u: + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: '@swc/core': '>=1.2.50' @@ -8591,7 +6148,7 @@ packages: optional: true dependencies: '@cspotcode/source-map-support': 0.8.1 - '@swc/core': 1.3.22 + '@swc/core': 1.3.38 '@tsconfig/node10': 1.0.9 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 @@ -8693,9 +6250,14 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - /type-fest/1.4.0: - resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} - engines: {node: '>=10'} + /type-fest/2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + dev: false + + /type-fest/3.6.1: + resolution: {integrity: sha512-htXWckxlT6U4+ilVgweNliPqlsVSSucbxVexRYllyMVJDtf5rTjv6kF/s+qAd4QSL1BZcnJPEJavYBPQiWuZDA==} + engines: {node: '>=14.16'} dev: false /typescript/4.7.4: @@ -8712,29 +6274,6 @@ packages: which-boxed-primitive: 1.0.2 dev: true - /unicode-canonical-property-names-ecmascript/2.0.0: - resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} - engines: {node: '>=4'} - dev: true - - /unicode-match-property-ecmascript/2.0.0: - resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} - engines: {node: '>=4'} - dependencies: - unicode-canonical-property-names-ecmascript: 2.0.0 - unicode-property-aliases-ecmascript: 2.0.0 - dev: true - - /unicode-match-property-value-ecmascript/2.0.0: - resolution: {integrity: sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==} - engines: {node: '>=4'} - dev: true - - /unicode-property-aliases-ecmascript/2.0.0: - resolution: {integrity: sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==} - engines: {node: '>=4'} - dev: true - /universalify/0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -8750,8 +6289,8 @@ packages: dependencies: punycode: 2.1.1 - /use-debounce/8.0.1_react@18.1.0: - resolution: {integrity: sha512-6tGAFJKJ0qCalecaV7/gm/M6n238nmitNppvR89ff1yfwSFjwFKR7IQZzIZf1KZRQhqNireBzytzU6jgb29oVg==} + /use-debounce/9.0.3_react@18.1.0: + resolution: {integrity: sha512-FhtlbDtDXILJV7Lix5OZj5yX/fW1tzq+VrvK1fnT2bUrPOGruU9Rw8NCEn+UI9wopfERBEZAOQ8lfeCJPllgnw==} engines: {node: '>= 10.0.0'} peerDependencies: react: '>=16.8.0' @@ -8790,6 +6329,7 @@ packages: '@jridgewell/trace-mapping': 0.3.15 '@types/istanbul-lib-coverage': 2.0.3 convert-source-map: 1.7.0 + dev: false /vite-plugin-ejs/1.6.4: resolution: {integrity: sha512-23p1RS4PiA0veXY5/gHZ60pl3pPvd8NEqdBsDgxNK8nM1rjFFDcVb0paNmuipzCgNP/Y0f/Id22M7Il4kvZ2jA==} @@ -8847,6 +6387,7 @@ packages: /w3c-hr-time/1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} + deprecated: Use your platform's native performance.now() and performance.timeOrigin. dependencies: browser-process-hrtime: 1.0.0 dev: true @@ -8862,6 +6403,7 @@ packages: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: makeerror: 1.0.12 + dev: false /warning/4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} @@ -8948,15 +6490,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /wrap-ansi/6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - dev: true - /wrap-ansi/7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -8968,12 +6501,13 @@ packages: /wrappy/1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - /write-file-atomic/4.0.1: - resolution: {integrity: sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16} + /write-file-atomic/4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} dependencies: imurmurhash: 0.1.4 signal-exit: 3.0.7 + dev: false /ws/8.8.0: resolution: {integrity: sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==} @@ -9013,11 +6547,6 @@ packages: engines: {node: '>= 6'} dev: true - /yaml/2.1.1: - resolution: {integrity: sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==} - engines: {node: '>= 14'} - dev: true - /yargs-parser/20.2.7: resolution: {integrity: sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==} engines: {node: '>=10'} @@ -9026,6 +6555,7 @@ packages: /yargs-parser/21.0.1: resolution: {integrity: sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==} engines: {node: '>=12'} + dev: false /yargs/16.2.0: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} @@ -9051,6 +6581,7 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.0.1 + dev: false /yn/3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} @@ -9059,18 +6590,15 @@ packages: /yocto-queue/0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + dev: false - /yup/0.32.11: - resolution: {integrity: sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==} - engines: {node: '>=10'} + /yup/1.0.2: + resolution: {integrity: sha512-Lpi8nITFKjWtCoK3yQP8MUk78LJmHWqbFd0OOMXTar+yjejlQ4OIIoZgnTW1bnEUKDw6dZBcy3/IdXnt2KDUow==} dependencies: - '@babel/runtime': 7.17.9 - '@types/lodash': 4.14.177 - lodash: 4.17.21 - lodash-es: 4.17.21 - nanoclone: 0.2.1 - property-expr: 2.0.4 + property-expr: 2.0.5 + tiny-case: 1.0.3 toposort: 2.0.2 + type-fest: 2.19.0 dev: false /zustand/4.1.1_react@18.1.0: diff --git a/kafka-ui-react-app/src/components/App.tsx b/kafka-ui-react-app/src/components/App.tsx index f5ac957e2e..e971b15e2b 100644 --- a/kafka-ui-react-app/src/components/App.tsx +++ b/kafka-ui-react-app/src/components/App.tsx @@ -5,10 +5,11 @@ import { clusterPath, errorPage, getNonExactPath, + clusterNewConfigPath, } from 'lib/paths'; import PageLoader from 'components/common/PageLoader/PageLoader'; import Dashboard from 'components/Dashboard/Dashboard'; -import ClusterPage from 'components/Cluster/Cluster'; +import ClusterPage from 'components/ClusterPage/ClusterPage'; import { ThemeProvider } from 'styled-components'; import theme from 'theme/theme'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -16,6 +17,7 @@ import { showServerError } from 'lib/errorHandling'; import { Toaster } from 'react-hot-toast'; import GlobalCSS from 'components/globalCss'; import * as S from 'components/App.styled'; +import ClusterConfigForm from 'widgets/ClusterConfigForm'; import ConfirmationModal from './common/ConfirmationModal/ConfirmationModal'; import { ConfirmContextProvider } from './contexts/ConfirmContext'; @@ -36,13 +38,12 @@ const queryClient = new QueryClient({ }, }, }); - const App: React.FC = () => { return ( - - - }> + + }> + @@ -56,6 +57,10 @@ const App: React.FC = () => { element={} /> ))} + } + /> } @@ -78,9 +83,9 @@ const App: React.FC = () => { - - - + + + ); }; diff --git a/kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.tsx b/kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.tsx index f1aa498811..966edecf1f 100644 --- a/kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.tsx +++ b/kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.tsx @@ -193,7 +193,7 @@ const BrokersList: React.FC = () => { onRowClick={({ original: { brokerId } }) => navigate(clusterBrokerPath(clusterName, brokerId)) } - emptyMessage="Disk usage data not available" + emptyMessage="No clusters are online" /> ); diff --git a/kafka-ui-react-app/src/components/Brokers/BrokersList/__test__/BrokersList.spec.tsx b/kafka-ui-react-app/src/components/Brokers/BrokersList/__test__/BrokersList.spec.tsx index cf36c64059..0c60cf4749 100644 --- a/kafka-ui-react-app/src/components/Brokers/BrokersList/__test__/BrokersList.spec.tsx +++ b/kafka-ui-react-app/src/components/Brokers/BrokersList/__test__/BrokersList.spec.tsx @@ -145,7 +145,7 @@ describe('BrokersList Component', () => { renderComponent(); expect(screen.getByRole('table')).toBeInTheDocument(); expect( - screen.getByRole('row', { name: 'Disk usage data not available' }) + screen.getByRole('row', { name: 'No clusters are online' }) ).toBeInTheDocument(); }); }); diff --git a/kafka-ui-react-app/src/components/ClusterPage/ClusterConfigPage.tsx b/kafka-ui-react-app/src/components/ClusterPage/ClusterConfigPage.tsx new file mode 100644 index 0000000000..b610114dd8 --- /dev/null +++ b/kafka-ui-react-app/src/components/ClusterPage/ClusterConfigPage.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { useAppConfig } from 'lib/hooks/api/appConfig'; +import useAppParams from 'lib/hooks/useAppParams'; +import { ClusterNameRoute } from 'lib/paths'; +import ClusterConfigForm from 'widgets/ClusterConfigForm'; +import { getInitialFormData } from 'widgets/ClusterConfigForm/utils/getInitialFormData'; + +const ClusterConfigPage: React.FC = () => { + const config = useAppConfig(); + const { clusterName } = useAppParams(); + + const currentClusterConfig = React.useMemo(() => { + if (config.isSuccess && !!config.data.properties?.kafka?.clusters) { + const current = config.data.properties?.kafka?.clusters?.find( + ({ name }) => name === clusterName + ); + if (current) { + return getInitialFormData(current); + } + } + return undefined; + }, [clusterName, config]); + + if (!currentClusterConfig) { + return null; + } + + const hasCustomConfig = Object.values(currentClusterConfig.customAuth).some( + (v) => !!v + ); + + return ( + + ); +}; + +export default ClusterConfigPage; diff --git a/kafka-ui-react-app/src/components/Cluster/Cluster.tsx b/kafka-ui-react-app/src/components/ClusterPage/ClusterPage.tsx similarity index 86% rename from kafka-ui-react-app/src/components/Cluster/Cluster.tsx rename to kafka-ui-react-app/src/components/ClusterPage/ClusterPage.tsx index 89c4931754..c2f6a13fbe 100644 --- a/kafka-ui-react-app/src/components/Cluster/Cluster.tsx +++ b/kafka-ui-react-app/src/components/ClusterPage/ClusterPage.tsx @@ -11,28 +11,34 @@ import { ClusterNameRoute, clusterSchemasRelativePath, clusterTopicsRelativePath, + clusterConfigRelativePath, getNonExactPath, } from 'lib/paths'; import ClusterContext from 'components/contexts/ClusterContext'; import PageLoader from 'components/common/PageLoader/PageLoader'; import { useClusters } from 'lib/hooks/api/clusters'; +import { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext'; const Brokers = React.lazy(() => import('components/Brokers/Brokers')); const Topics = React.lazy(() => import('components/Topics/Topics')); const Schemas = React.lazy(() => import('components/Schemas/Schemas')); const Connect = React.lazy(() => import('components/Connect/Connect')); const KsqlDb = React.lazy(() => import('components/KsqlDb/KsqlDb')); +const ClusterConfigPage = React.lazy( + () => import('components/ClusterPage/ClusterConfigPage') +); const ConsumerGroups = React.lazy( () => import('components/ConsumerGroups/ConsumerGroups') ); -const Cluster: React.FC = () => { +const ClusterPage: React.FC = () => { const { clusterName } = useAppParams(); + const appInfo = React.useContext(GlobalSettingsContext); + const { data } = useClusters(); const contextValue = React.useMemo(() => { const cluster = data?.find(({ name }) => name === clusterName); const features = cluster?.features || []; - return { isReadOnly: cluster?.readOnly || false, hasKafkaConnectConfigured: features.includes( @@ -89,6 +95,12 @@ const Cluster: React.FC = () => { element={} /> )} + {appInfo.hasDynamicConfig && ( + } + /> + )} } @@ -101,4 +113,4 @@ const Cluster: React.FC = () => { ); }; -export default Cluster; +export default ClusterPage; diff --git a/kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx b/kafka-ui-react-app/src/components/ClusterPage/__tests__/ClusterPage.spec.tsx similarity index 96% rename from kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx rename to kafka-ui-react-app/src/components/ClusterPage/__tests__/ClusterPage.spec.tsx index d7b2c6d2bb..b66fd0a0b3 100644 --- a/kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx +++ b/kafka-ui-react-app/src/components/ClusterPage/__tests__/ClusterPage.spec.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Cluster, ClusterFeaturesEnum } from 'generated-sources'; -import ClusterComponent from 'components/Cluster/Cluster'; +import ClusterPageComponent from 'components/ClusterPage/ClusterPage'; import { screen, waitFor } from '@testing-library/react'; import { render, WithRoute } from 'lib/testHelpers'; import { @@ -48,14 +48,14 @@ jest.mock('lib/hooks/api/clusters', () => ({ useClusters: jest.fn(), })); -describe('Cluster', () => { +describe('ClusterPage', () => { const renderComponent = async (pathname: string, payload: Cluster[] = []) => { (useClusters as jest.Mock).mockImplementation(() => ({ data: payload, })); await render( - + , { initialEntries: [pathname] } ); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Actions/Action.styled.ts b/kafka-ui-react-app/src/components/Connect/Details/Actions/Action.styled.ts new file mode 100644 index 0000000000..a2998acdc2 --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/Details/Actions/Action.styled.ts @@ -0,0 +1,23 @@ +import styled from 'styled-components'; + +export const ConnectorActionsWrapperStyled = styled.div` + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +`; +export const ButtonLabel = styled.span` + margin-right: 11.5px; +`; +export const RestartButton = styled.div` + padding: 0 12px; + border: none; + border-radius: 4px; + display: flex; + -webkit-align-items: center; + background: #e8e8fc; + color: #171a1c; + font-size: 14px; + font-weight: 500; + height: 32px; +`; diff --git a/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx b/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx index f475b739e1..530f0db147 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import styled from 'styled-components'; import { useNavigate } from 'react-router-dom'; import { useIsMutating } from '@tanstack/react-query'; import { @@ -21,13 +20,9 @@ import { import { useConfirm } from 'lib/hooks/useConfirm'; import { Dropdown } from 'components/common/Dropdown'; import { ActionDropdownItem } from 'components/common/ActionComponent'; +import ChevronDownIcon from 'components/common/Icons/ChevronDownIcon'; -const ConnectorActionsWrapperStyled = styled.div` - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; -`; +import * as S from './Action.styled'; const Actions: React.FC = () => { const navigate = useNavigate(); @@ -66,10 +61,16 @@ const Actions: React.FC = () => { stateMutation.mutateAsync(ConnectorAction.PAUSE); const resumeConnectorHandler = () => stateMutation.mutateAsync(ConnectorAction.RESUME); - return ( - - + + + Restart + + + } + > {connector?.status.state === ConnectorState.RUNNING && ( { > Restart Failed Tasks + + { Delete - + ); }; diff --git a/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx index c3c4cff8f1..9dce7507f5 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx @@ -35,8 +35,11 @@ const expectActionButtonsExists = () => { }; const afterClickDropDownButton = async () => { const dropDownButton = screen.getAllByRole('button'); - expect(dropDownButton.length).toEqual(1); - await userEvent.click(dropDownButton[0]); + await userEvent.click(dropDownButton[1]); +}; +const afterClickRestartButton = async () => { + const dropDownButton = screen.getByText('Restart'); + await userEvent.click(dropDownButton); }; describe('Actions', () => { afterEach(() => { @@ -66,8 +69,8 @@ describe('Actions', () => { data: set({ ...connector }, 'status.state', ConnectorState.PAUSED), })); renderComponent(); - await afterClickDropDownButton(); - expect(screen.getAllByRole('menuitem').length).toEqual(5); + await afterClickRestartButton(); + expect(screen.getAllByRole('menuitem').length).toEqual(4); expect(screen.getByText('Resume')).toBeInTheDocument(); expect(screen.queryByText('Pause')).not.toBeInTheDocument(); expectActionButtonsExists(); @@ -78,8 +81,8 @@ describe('Actions', () => { data: set({ ...connector }, 'status.state', ConnectorState.FAILED), })); renderComponent(); - await afterClickDropDownButton(); - expect(screen.getAllByRole('menuitem').length).toEqual(4); + await afterClickRestartButton(); + expect(screen.getAllByRole('menuitem').length).toEqual(3); expect(screen.queryByText('Resume')).not.toBeInTheDocument(); expect(screen.queryByText('Pause')).not.toBeInTheDocument(); expectActionButtonsExists(); @@ -90,8 +93,8 @@ describe('Actions', () => { data: set({ ...connector }, 'status.state', ConnectorState.UNASSIGNED), })); renderComponent(); - await afterClickDropDownButton(); - expect(screen.getAllByRole('menuitem').length).toEqual(4); + await afterClickRestartButton(); + expect(screen.getAllByRole('menuitem').length).toEqual(3); expect(screen.queryByText('Resume')).not.toBeInTheDocument(); expect(screen.queryByText('Pause')).not.toBeInTheDocument(); expectActionButtonsExists(); @@ -102,8 +105,8 @@ describe('Actions', () => { data: set({ ...connector }, 'status.state', ConnectorState.RUNNING), })); renderComponent(); - await afterClickDropDownButton(); - expect(screen.getAllByRole('menuitem').length).toEqual(5); + await afterClickRestartButton(); + expect(screen.getAllByRole('menuitem').length).toEqual(4); expect(screen.queryByText('Resume')).not.toBeInTheDocument(); expect(screen.getByText('Pause')).toBeInTheDocument(); expectActionButtonsExists(); @@ -131,7 +134,7 @@ describe('Actions', () => { mutateAsync: restartConnector, })); renderComponent(); - await afterClickDropDownButton(); + await afterClickRestartButton(); await userEvent.click( screen.getByRole('menuitem', { name: 'Restart Connector' }) ); @@ -144,7 +147,7 @@ describe('Actions', () => { mutateAsync: restartAllTasks, })); renderComponent(); - await afterClickDropDownButton(); + await afterClickRestartButton(); await userEvent.click( screen.getByRole('menuitem', { name: 'Restart All Tasks' }) ); @@ -159,7 +162,7 @@ describe('Actions', () => { mutateAsync: restartFailedTasks, })); renderComponent(); - await afterClickDropDownButton(); + await afterClickRestartButton(); await userEvent.click( screen.getByRole('menuitem', { name: 'Restart Failed Tasks' }) ); @@ -174,7 +177,7 @@ describe('Actions', () => { mutateAsync: pauseConnector, })); renderComponent(); - await afterClickDropDownButton(); + await afterClickRestartButton(); await userEvent.click(screen.getByRole('menuitem', { name: 'Pause' })); expect(pauseConnector).toHaveBeenCalledWith(ConnectorAction.PAUSE); }); @@ -188,7 +191,7 @@ describe('Actions', () => { mutateAsync: resumeConnector, })); renderComponent(); - await afterClickDropDownButton(); + await afterClickRestartButton(); await userEvent.click(screen.getByRole('menuitem', { name: 'Resume' })); expect(resumeConnector).toHaveBeenCalledWith(ConnectorAction.RESUME); }); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx b/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx index f8a0ada02f..0e86d48940 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx @@ -51,9 +51,13 @@ const Config: React.FC = () => { }, [config, setValue]); const onSubmit = async (values: FormValues) => { - const requestBody = JSON.parse(values.config.trim()); - await mutation.mutateAsync(requestBody); - reset(values); + try { + const requestBody = JSON.parse(values.config.trim()); + await mutation.mutateAsync(requestBody); + reset(values); + } catch (e) { + // do nothing + } }; const hasCredentials = JSON.stringify(config, null, '\t').includes( diff --git a/kafka-ui-react-app/src/components/Connect/New/New.tsx b/kafka-ui-react-app/src/components/Connect/New/New.tsx index 2e39935942..8040267661 100644 --- a/kafka-ui-react-app/src/components/Connect/New/New.tsx +++ b/kafka-ui-react-app/src/components/Connect/New/New.tsx @@ -65,22 +65,26 @@ const New: React.FC = () => { }, [connects, getValues, setValue]); const onSubmit = async (values: FormValues) => { - const connector = await mutation.createResource({ - connectName: values.connectName, - newConnector: { - name: values.name, - config: JSON.parse(values.config.trim()), - }, - }); + try { + const connector = await mutation.createResource({ + connectName: values.connectName, + newConnector: { + name: values.name, + config: JSON.parse(values.config.trim()), + }, + }); - if (connector) { - navigate( - clusterConnectConnectorPath( - clusterName, - connector.connect, - connector.name - ) - ); + if (connector) { + navigate( + clusterConnectConnectorPath( + clusterName, + connector.connect, + connector.name + ) + ); + } + } catch (e) { + // do nothing } }; @@ -133,6 +137,7 @@ const New: React.FC = () => { inputSize="M" placeholder="Connector Name" name="name" + autoFocus autoComplete="off" disabled={isSubmitting} /> diff --git a/kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx b/kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx index 88c79d51cb..1284bfe652 100644 --- a/kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx @@ -84,7 +84,7 @@ describe('New', () => { return Promise.resolve(); }); (useCreateConnector as jest.Mock).mockImplementation(() => ({ - mutateAsync: createConnectorMock, + createResource: createConnectorMock, })); renderComponent(); await simulateFormSubmit(); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/ResetOffsets.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/ResetOffsets.spec.tsx index 9b0682f04f..21bcd56087 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/ResetOffsets.spec.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/ResetOffsets.spec.tsx @@ -36,7 +36,6 @@ const selectresetTypeAndPartitions = async (resetType: string) => { await userEvent.click(screen.getByLabelText('Reset Type')); await userEvent.click(screen.getByText(resetType)); await userEvent.click(screen.getByText('Select...')); - await userEvent.click(screen.getByText('Partition #0')); }; @@ -72,12 +71,14 @@ describe('ResetOffsets', () => { fetchMock.reset(); }); - it('renders progress bar for initial state', async () => { + xit('renders progress bar for initial state', async () => { fetchMock.getOnce( `/api/clusters/${clusterName}/consumer-groups/${groupId}`, 404 ); - await waitFor(() => renderComponent()); + await act(() => { + renderComponent(); + }); expect(screen.getByRole('progressbar')).toBeInTheDocument(); }); @@ -117,14 +118,13 @@ describe('ResetOffsets', () => { ); await userEvent.click(screen.getAllByLabelText('Partition #0')[1]); - await userEvent.keyboard('10'); - await userEvent.click(screen.getByText('Submit')); - await resetConsumerGroupOffsetsMockCalled(); }); - it('calls resetConsumerGroupOffsets with TIMESTAMP', async () => { + + // focus doesn't work for datepicker + it.skip('calls resetConsumerGroupOffsets with TIMESTAMP', async () => { await selectresetTypeAndPartitions('TIMESTAMP'); const resetConsumerGroupOffsetsMock = fetchMock.postOnce( `/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets`, diff --git a/kafka-ui-react-app/src/components/Dashboard/ClusterName.tsx b/kafka-ui-react-app/src/components/Dashboard/ClusterName.tsx new file mode 100644 index 0000000000..7ab581d405 --- /dev/null +++ b/kafka-ui-react-app/src/components/Dashboard/ClusterName.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { CellContext } from '@tanstack/react-table'; +import { Tag } from 'components/common/Tag/Tag.styled'; +import { Cluster } from 'generated-sources'; + +type ClusterNameProps = CellContext; + +const ClusterName: React.FC = ({ row }) => { + const { readOnly, name } = row.original; + return ( + <> + {readOnly && readonly} + {name} + + ); +}; + +export default ClusterName; diff --git a/kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx b/kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx new file mode 100644 index 0000000000..cb41ab06a8 --- /dev/null +++ b/kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Cluster } from 'generated-sources'; +import { CellContext } from '@tanstack/react-table'; +import { Button } from 'components/common/Button/Button'; +import { clusterConfigPath } from 'lib/paths'; + +type Props = CellContext; + +const ClusterTableActionsCell: React.FC = ({ row }) => { + const { name } = row.original; + return ( + + ); +}; + +export default ClusterTableActionsCell; diff --git a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClusterName.tsx b/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClusterName.tsx deleted file mode 100644 index 52702c348a..0000000000 --- a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClusterName.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { CellContext } from '@tanstack/react-table'; -import { Tag } from 'components/common/Tag/Tag.styled'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const ClusterName: React.FC> = ({ row }) => { - return ( - <> - {row.original.readOnly && readonly} - {row.original.name} - - ); -}; - -export default ClusterName; diff --git a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.styled.ts b/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.styled.ts deleted file mode 100644 index 341cb245ff..0000000000 --- a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.styled.ts +++ /dev/null @@ -1,15 +0,0 @@ -import styled from 'styled-components'; - -interface TableCellProps { - maxWidth?: string; -} - -export const SwitchWrapper = styled.div` - padding: 16px; -`; - -export const TableCell = styled.td.attrs({ role: 'cells' })` - padding: 16px; - word-break: break-word; - max-width: ${(props) => props.maxWidth}; -`; diff --git a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.tsx b/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.tsx deleted file mode 100644 index 0327a01001..0000000000 --- a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import * as Metrics from 'components/common/Metrics'; -import { Tag } from 'components/common/Tag/Tag.styled'; -import Switch from 'components/common/Switch/Switch'; -import { useClusters } from 'lib/hooks/api/clusters'; -import { Cluster, ServerStatus } from 'generated-sources'; -import { ColumnDef } from '@tanstack/react-table'; -import Table, { SizeCell } from 'components/common/NewTable'; - -import * as S from './ClustersWidget.styled'; -import ClusterName from './ClusterName'; - -const ClustersWidget: React.FC = () => { - const { data } = useClusters(); - const [showOfflineOnly, setShowOfflineOnly] = React.useState(false); - - const config = React.useMemo(() => { - const clusters = data || []; - const offlineClusters = clusters.filter( - ({ status }) => status === ServerStatus.OFFLINE - ); - return { - list: showOfflineOnly ? offlineClusters : clusters, - online: clusters.length - offlineClusters.length, - offline: offlineClusters.length, - }; - }, [data, showOfflineOnly]); - - const columns = React.useMemo[]>( - () => [ - { header: 'Cluster name', accessorKey: 'name', cell: ClusterName }, - { header: 'Version', accessorKey: 'version' }, - { header: 'Brokers count', accessorKey: 'brokerCount' }, - { header: 'Partitions', accessorKey: 'onlinePartitionCount' }, - { header: 'Topics', accessorKey: 'topicCount' }, - { header: 'Production', accessorKey: 'bytesInPerSec', cell: SizeCell }, - { header: 'Consumption', accessorKey: 'bytesOutPerSec', cell: SizeCell }, - ], - [] - ); - - const handleSwitch = () => setShowOfflineOnly(!showOfflineOnly); - return ( - <> - - - Online}> - {config.online}{' '} - clusters - - Offline}> - {config.offline}{' '} - clusters - - - - - - - - - - ); -}; - -export default ClustersWidget; diff --git a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidget.spec.tsx b/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidget.spec.tsx deleted file mode 100644 index 2d6f967e2c..0000000000 --- a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidget.spec.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { screen } from '@testing-library/react'; -import ClustersWidget from 'components/Dashboard/ClustersWidget/ClustersWidget'; -import userEvent from '@testing-library/user-event'; -import { render } from 'lib/testHelpers'; -import { useClusters } from 'lib/hooks/api/clusters'; -import { clustersPayload } from 'lib/fixtures/clusters'; - -jest.mock('lib/hooks/api/clusters', () => ({ - useClusters: jest.fn(), -})); - -describe('ClustersWidget', () => { - beforeEach(async () => { - (useClusters as jest.Mock).mockImplementation(() => ({ - data: clustersPayload, - isSuccess: true, - })); - await render(); - }); - - it('renders clusterWidget list', () => { - expect(screen.getAllByRole('row').length).toBe(3); - }); - - it('hides online cluster widgets', async () => { - expect(screen.getAllByRole('row').length).toBe(3); - await userEvent.click(screen.getByRole('checkbox')); - expect(screen.getAllByRole('row').length).toBe(2); - }); - - it('when cluster is read-only', () => { - expect(screen.getByText('readonly')).toBeInTheDocument(); - }); - - it('render clusterWidget cells', () => { - const cells = screen.getAllByRole('cell'); - expect(cells.length).toBe(14); - }); -}); diff --git a/kafka-ui-react-app/src/components/Dashboard/Dashboard.styled.ts b/kafka-ui-react-app/src/components/Dashboard/Dashboard.styled.ts new file mode 100644 index 0000000000..a35424b1bb --- /dev/null +++ b/kafka-ui-react-app/src/components/Dashboard/Dashboard.styled.ts @@ -0,0 +1,8 @@ +import styled from 'styled-components'; + +export const Toolbar = styled.div` + padding: 8px 16px; + display: flex; + justify-content: space-between; + align-items: center; +`; diff --git a/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx b/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx index 518c9e4433..46c35d5079 100644 --- a/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx +++ b/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx @@ -1,14 +1,98 @@ -import React, { Suspense } from 'react'; +import React from 'react'; import PageHeading from 'components/common/PageHeading/PageHeading'; -import ClustersWidget from 'components/Dashboard/ClustersWidget/ClustersWidget'; +import * as Metrics from 'components/common/Metrics'; +import { Tag } from 'components/common/Tag/Tag.styled'; +import Switch from 'components/common/Switch/Switch'; +import { useClusters } from 'lib/hooks/api/clusters'; +import { Cluster, ServerStatus } from 'generated-sources'; +import { ColumnDef } from '@tanstack/react-table'; +import Table, { SizeCell } from 'components/common/NewTable'; +import useBoolean from 'lib/hooks/useBoolean'; +import { Button } from 'components/common/Button/Button'; +import { clusterNewConfigPath } from 'lib/paths'; +import { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext'; -const Dashboard: React.FC = () => ( - <> - - - - - -); +import * as S from './Dashboard.styled'; +import ClusterName from './ClusterName'; +import ClusterTableActionsCell from './ClusterTableActionsCell'; + +const Dashboard: React.FC = () => { + const clusters = useClusters(); + const { value: showOfflineOnly, toggle } = useBoolean(false); + const appInfo = React.useContext(GlobalSettingsContext); + + const config = React.useMemo(() => { + const clusterList = clusters.data || []; + const offlineClusters = clusterList.filter( + ({ status }) => status === ServerStatus.OFFLINE + ); + return { + list: showOfflineOnly ? offlineClusters : clusterList, + online: clusterList.length - offlineClusters.length, + offline: offlineClusters.length, + }; + }, [clusters, showOfflineOnly]); + + const columns = React.useMemo[]>(() => { + const initialColumns: ColumnDef[] = [ + { header: 'Cluster name', accessorKey: 'name', cell: ClusterName }, + { header: 'Version', accessorKey: 'version' }, + { header: 'Brokers count', accessorKey: 'brokerCount' }, + { header: 'Partitions', accessorKey: 'onlinePartitionCount' }, + { header: 'Topics', accessorKey: 'topicCount' }, + { header: 'Production', accessorKey: 'bytesInPerSec', cell: SizeCell }, + { header: 'Consumption', accessorKey: 'bytesOutPerSec', cell: SizeCell }, + ]; + + if (appInfo.hasDynamicConfig) { + initialColumns.push({ + header: '', + id: 'actions', + cell: ClusterTableActionsCell, + }); + } + + return initialColumns; + }, []); + + return ( + <> + + + + Online}> + {config.online || 0}{' '} + clusters + + Offline}> + {config.offline || 0}{' '} + clusters + + + + +
+ + +
+ {appInfo.hasDynamicConfig && ( + + )} +
+
+ + ); +}; export default Dashboard; diff --git a/kafka-ui-react-app/src/components/Dashboard/__test__/Dashboard.spec.tsx b/kafka-ui-react-app/src/components/Dashboard/__test__/Dashboard.spec.tsx deleted file mode 100644 index 2e5d00cd2d..0000000000 --- a/kafka-ui-react-app/src/components/Dashboard/__test__/Dashboard.spec.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import Dashboard from 'components/Dashboard/Dashboard'; -import { render } from 'lib/testHelpers'; -import { screen } from '@testing-library/dom'; - -jest.mock('components/Dashboard/ClustersWidget/ClustersWidget', () => () => ( -
mock-ClustersWidget
-)); - -describe('Dashboard', () => { - it('renders ClustersWidget', () => { - render(); - expect(screen.getByText('Dashboard')).toBeInTheDocument(); - expect(screen.getByText('mock-ClustersWidget')).toBeInTheDocument(); - }); -}); diff --git a/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx b/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx index dff0e7f3ae..8ba2cba8e9 100644 --- a/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx +++ b/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx @@ -42,7 +42,6 @@ const ClusterMenu: React.FC = ({ to={clusterConsumerGroupsPath(name)} title="Consumers" /> - {hasFeatureConfigured(ClusterFeaturesEnum.SCHEMA_REGISTRY) && ( { - const query = useClusters(); - - if (!query.isSuccess) { - return null; - } + const clusters = useClusters(); return ( ); }; diff --git a/kafka-ui-react-app/src/components/NavBar/UserInfo/UserInfo.tsx b/kafka-ui-react-app/src/components/NavBar/UserInfo/UserInfo.tsx index bae205ad03..a70276afab 100644 --- a/kafka-ui-react-app/src/components/NavBar/UserInfo/UserInfo.tsx +++ b/kafka-ui-react-app/src/components/NavBar/UserInfo/UserInfo.tsx @@ -26,7 +26,7 @@ const UserInfo = () => { } > - Log out + Log out ) : null; diff --git a/kafka-ui-react-app/src/components/NavBar/UserInfo/__tests__/UserInfo.spec.tsx b/kafka-ui-react-app/src/components/NavBar/UserInfo/__tests__/UserInfo.spec.tsx index f474783c47..b51f00da02 100644 --- a/kafka-ui-react-app/src/components/NavBar/UserInfo/__tests__/UserInfo.spec.tsx +++ b/kafka-ui-react-app/src/components/NavBar/UserInfo/__tests__/UserInfo.spec.tsx @@ -22,6 +22,10 @@ describe('UserInfo', () => { it('should render the userInfo during click opens the dropdown', async () => { const username = 'someName'; + Object.defineProperty(window, 'basePath', { + value: '', + writable: true, + }); (useUserInfo as jest.Mock).mockImplementation(() => ({ username })); renderComponent(); @@ -33,6 +37,22 @@ describe('UserInfo', () => { expect(logout).toHaveAttribute('href', '/logout'); }); + it('should render correct url during basePath initialization', async () => { + const username = 'someName'; + const baseUrl = '/path'; + Object.defineProperty(window, 'basePath', { + value: baseUrl, + writable: true, + }); + (useUserInfo as jest.Mock).mockImplementation(() => ({ username })); + + renderComponent(); + + const logout = screen.getByText('Log out'); + expect(logout).toBeInTheDocument(); + expect(logout).toHaveAttribute('href', `${baseUrl}/logout`); + }); + it('should not render anything if the username does not exists', () => { (useUserInfo as jest.Mock).mockImplementation(() => ({ username: undefined, diff --git a/kafka-ui-react-app/src/components/PageContainer/PageContainer.tsx b/kafka-ui-react-app/src/components/PageContainer/PageContainer.tsx index 3459e84ec2..93fe4dabe5 100644 --- a/kafka-ui-react-app/src/components/PageContainer/PageContainer.tsx +++ b/kafka-ui-react-app/src/components/PageContainer/PageContainer.tsx @@ -1,14 +1,16 @@ -import React, { PropsWithChildren, Suspense, useCallback } from 'react'; +import React, { PropsWithChildren } from 'react'; import { useLocation } from 'react-router-dom'; import NavBar from 'components/NavBar/NavBar'; import * as S from 'components/PageContainer/PageContainer.styled'; -import PageLoader from 'components/common/PageLoader/PageLoader'; import Nav from 'components/Nav/Nav'; +import useBoolean from 'lib/hooks/useBoolean'; const PageContainer: React.FC> = ({ children }) => { - const [isSidebarVisible, setIsSidebarVisible] = React.useState(false); - const onBurgerClick = () => setIsSidebarVisible(!isSidebarVisible); - const closeSidebar = useCallback(() => setIsSidebarVisible(false), []); + const { + value: isSidebarVisible, + toggle, + setFalse: closeSidebar, + } = useBoolean(false); const location = useLocation(); React.useEffect(() => { @@ -17,12 +19,10 @@ const PageContainer: React.FC> = ({ children }) => { return ( <> - + - }> -
@@ -77,18 +88,6 @@ const MessagesTable: React.FC = () => { onPreview={() => setPreviewFor('content')} /> - - {previewFor !== null && ( - setPreviewFor(null)} - setFilters={(payload: PreviewFilter[]) => - previewFor === 'key' - ? setKeyFilters(payload) - : setContentFilters(payload) - } - /> - )} @@ -139,7 +138,7 @@ const MessagesTable: React.FC = () => { - + ); }; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/PreviewModal.styled.ts b/kafka-ui-react-app/src/components/Topics/Topic/Messages/PreviewModal.styled.ts index 35f221ddea..c1cecfbbd0 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/PreviewModal.styled.ts +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/PreviewModal.styled.ts @@ -7,6 +7,7 @@ export const PreviewModal = styled.div` background: ${({ theme }) => theme.modal.backgroundColor}; position: absolute; left: 25%; + top: 30px; // some margin border: 1px solid ${({ theme }) => theme.modal.border.contrast}; box-shadow: ${({ theme }) => theme.modal.shadow}; padding: 32px; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Messages.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Messages.spec.tsx index e3a4db8f44..172dc81013 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Messages.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Messages.spec.tsx @@ -78,11 +78,13 @@ describe('Messages', () => { const liveOptionConf = SeekDirectionOptions[2]; const labelValue2 = liveOptionConf.label; await userEvent.click(seekDirectionSelect); - const liveModeLi = screen.getByRole( - (role, element) => - role === 'option' && - element?.getAttribute('value') === liveOptionConf.value + + const options = screen.getAllByRole('option'); + const liveModeLi = options.find( + (option) => option.getAttribute('value') === liveOptionConf.value ); + expect(liveModeLi).toBeInTheDocument(); + if (!liveModeLi) return; // to make TS happy await userEvent.selectOptions(seekDirectionSelect, [liveModeLi]); expect(seekDirectionOption).toHaveTextContent(labelValue2); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.styled.ts b/kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.styled.ts index a64db26117..d274a1ba9d 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.styled.ts +++ b/kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.styled.ts @@ -2,9 +2,15 @@ import styled from 'styled-components'; export const Replica = styled.span.attrs({ 'aria-label': 'replica-info' })<{ leader?: boolean; + outOfSync?: boolean; }>` - color: ${({ leader, theme }) => - leader ? theme.topicMetaData.liderReplica.color : null}; + color: ${({ leader, outOfSync, theme }) => { + if (outOfSync) return theme.topicMetaData.outOfSync.color; + if (leader) return theme.topicMetaData.liderReplica.color; + return null; + }}; + + font-weight: ${({ outOfSync }) => (outOfSync ? '500' : null)}; &:after { content: ', '; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.tsx index f2f133f89d..c639ac2b62 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.tsx @@ -51,9 +51,10 @@ const Overview: React.FC = () => { if (replicas === undefined || replicas.length === 0) { return 0; } - return replicas?.map(({ broker, leader }: Replica) => ( + return replicas?.map(({ broker, leader, inSync }: Replica) => ( diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Overview/__test__/Overview.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Overview/__test__/Overview.spec.tsx index d248757853..d02b2b228c 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Overview/__test__/Overview.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Overview/__test__/Overview.spec.tsx @@ -75,6 +75,19 @@ describe('Overview', () => { ); }); + describe('when replicas out of sync', () => { + it('should be the appropriate color', () => { + render(); + const element = screen.getByLabelText('replica-info'); + expect(element).toBeInTheDocument(); + expect(element).toHaveStyleRule( + 'color', + theme.topicMetaData.outOfSync.color + ); + expect(element).toHaveStyleRule('font-weight', '500'); + }); + }); + describe('when it has internal flag', () => { it('renders the Action button for Topic', () => { renderComponent({ diff --git a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx index b0c35fc750..9450e512ad 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx @@ -101,16 +101,19 @@ const SendMessage: React.FC<{ onSubmit: () => void }> = ({ onSubmit }) => { }); return; } - - await sendMessage.mutateAsync({ - key: key || null, - content: content || null, - headers: parsedHeaders, - partition: partition || 0, - keySerde, - valueSerde, - }); - onSubmit(); + try { + await sendMessage.mutateAsync({ + key: key || null, + content: content || null, + headers: parsedHeaders, + partition: partition || 0, + keySerde, + valueSerde, + }); + onSubmit(); + } catch (e) { + // do nothing + } }; return ( diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx index 15f60eaaac..e8d7f1b844 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx @@ -93,6 +93,7 @@ const TopicForm: React.FC = ({ Topic Name * = ({ placeholder="Number of partitions" min="1" name="partitions" + positiveOnly + integerOnly /> @@ -160,6 +163,8 @@ const TopicForm: React.FC = ({ placeholder="Min In Sync Replicas" min="1" name="minInSyncReplicas" + positiveOnly + integerOnly /> @@ -176,6 +181,8 @@ const TopicForm: React.FC = ({ placeholder="Replication Factor" min="1" name="replicationFactor" + positiveOnly + integerOnly /> @@ -226,6 +233,8 @@ const TopicForm: React.FC = ({ placeholder="Maximum message size" min="1" name="maxMessageBytes" + positiveOnly + integerOnly /> diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.spec.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.spec.tsx index c1006b6d7e..e3fd7a7e5e 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.spec.tsx @@ -1,6 +1,6 @@ import React, { PropsWithChildren } from 'react'; import { render } from 'lib/testHelpers'; -import { fireEvent, screen } from '@testing-library/dom'; +import { screen } from '@testing-library/dom'; import { FormProvider, useForm } from 'react-hook-form'; import TopicForm, { Props } from 'components/Topics/shared/Form/TopicForm'; import userEvent from '@testing-library/user-event'; @@ -30,8 +30,10 @@ const expectByRoleAndNameToBeInDocument = ( }; describe('TopicForm', () => { - it('renders', () => { - renderComponent(); + it('renders', async () => { + await act(async () => { + renderComponent(); + }); expectByRoleAndNameToBeInDocument('textbox', 'Topic Name *'); @@ -62,22 +64,19 @@ describe('TopicForm', () => { }); it('submits', async () => { - renderComponent({ - isSubmitting, - onSubmit: onSubmit.mockImplementation((e) => e.preventDefault()), - }); - await act(async () => { - await userEvent.type( - screen.getByPlaceholderText('Topic Name'), - 'topicName' - ); - }); - await act(() => { - fireEvent.submit(screen.getByLabelText('topic form')); + renderComponent({ + isSubmitting, + onSubmit: onSubmit.mockImplementation((e) => e.preventDefault()), + }); }); + await userEvent.type( + screen.getByPlaceholderText('Topic Name'), + 'topicName' + ); await userEvent.click(screen.getByRole('button', { name: 'Create topic' })); + expect(onSubmit).toBeCalledTimes(1); }); }); diff --git a/kafka-ui-react-app/src/components/Version/Version.tsx b/kafka-ui-react-app/src/components/Version/Version.tsx index 2a91f9ec3b..2a611ae4a5 100644 --- a/kafka-ui-react-app/src/components/Version/Version.tsx +++ b/kafka-ui-react-app/src/components/Version/Version.tsx @@ -9,11 +9,6 @@ import { formatTimestamp } from 'lib/dateTimeHelpers'; import * as S from './Version.styled'; import compareVersions from './compareVersions'; -export interface VesionProps { - tag: string; - commit?: string; -} - const Version: React.FC = () => { const { data: actuatorInfo = {} } = useActuatorInfo(); const { data: latestVersionInfo = {} } = useLatestVersion(); diff --git a/kafka-ui-react-app/src/components/__tests__/App.spec.tsx b/kafka-ui-react-app/src/components/__tests__/App.spec.tsx index 6157c2291e..32ccf009e6 100644 --- a/kafka-ui-react-app/src/components/__tests__/App.spec.tsx +++ b/kafka-ui-react-app/src/components/__tests__/App.spec.tsx @@ -3,6 +3,7 @@ import { screen } from '@testing-library/react'; import App from 'components/App'; import { render } from 'lib/testHelpers'; import { useGetUserInfo } from 'lib/hooks/api/roles'; +import { useAppInfo } from 'lib/hooks/api/appConfig'; jest.mock('components/Nav/Nav', () => () =>
Navigation
); @@ -11,12 +12,18 @@ jest.mock('components/Version/Version', () => () =>
Version
); jest.mock('lib/hooks/api/roles', () => ({ useGetUserInfo: jest.fn(), })); +jest.mock('lib/hooks/api/appConfig', () => ({ + useAppInfo: jest.fn(), +})); describe('App', () => { beforeEach(() => { (useGetUserInfo as jest.Mock).mockImplementation(() => ({ data: {}, })); + (useAppInfo as jest.Mock).mockImplementation(() => ({ + data: {}, + })); render(, { initialEntries: ['/'], diff --git a/kafka-ui-react-app/src/components/common/ActionComponent/ActionComponent.ts b/kafka-ui-react-app/src/components/common/ActionComponent/ActionComponent.ts index 346b916ca1..4d6e593ef2 100644 --- a/kafka-ui-react-app/src/components/common/ActionComponent/ActionComponent.ts +++ b/kafka-ui-react-app/src/components/common/ActionComponent/ActionComponent.ts @@ -1,4 +1,4 @@ -import { Placement } from '@floating-ui/react-dom-interactions'; +import { Placement } from '@floating-ui/react'; import { Action, ResourceType } from 'generated-sources'; export interface ActionComponentProps { diff --git a/kafka-ui-react-app/src/components/common/Alert/Alert.styled.ts b/kafka-ui-react-app/src/components/common/Alert/Alert.styled.ts index 58ae2ed09d..eefd3bd3be 100644 --- a/kafka-ui-react-app/src/components/common/Alert/Alert.styled.ts +++ b/kafka-ui-react-app/src/components/common/Alert/Alert.styled.ts @@ -1,7 +1,7 @@ -import { ToastType } from 'react-hot-toast'; import styled from 'styled-components'; +import { ToastTypes } from 'lib/errorHandling'; -export const Alert = styled.div<{ $type: ToastType }>` +export const Alert = styled.div<{ $type: ToastTypes }>` background-color: ${({ $type, theme }) => theme.alert.color[$type]}; width: 500px; min-height: 64px; diff --git a/kafka-ui-react-app/src/components/common/Alert/Alert.tsx b/kafka-ui-react-app/src/components/common/Alert/Alert.tsx index 8f9d167d1e..5b58a573d4 100644 --- a/kafka-ui-react-app/src/components/common/Alert/Alert.tsx +++ b/kafka-ui-react-app/src/components/common/Alert/Alert.tsx @@ -1,13 +1,13 @@ import React from 'react'; import CloseIcon from 'components/common/Icons/CloseIcon'; import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper'; -import { ToastType } from 'react-hot-toast'; +import { ToastTypes } from 'lib/errorHandling'; import * as S from './Alert.styled'; export interface AlertProps { title: string; - type: ToastType; + type: ToastTypes; message: React.ReactNode; onDissmiss(): void; } diff --git a/kafka-ui-react-app/src/components/common/Button/Button.styled.ts b/kafka-ui-react-app/src/components/common/Button/Button.styled.ts index 574e8a6c5e..4cf793a5ed 100644 --- a/kafka-ui-react-app/src/components/common/Button/Button.styled.ts +++ b/kafka-ui-react-app/src/components/common/Button/Button.styled.ts @@ -58,8 +58,10 @@ const StyledButton = styled.button` color: ${(props) => props.theme.button.primary.color}; } - & svg { - margin-right: 7px; + & :first-of-type { + svg { + margin-right: 7px; + } } `; diff --git a/kafka-ui-react-app/src/components/common/Button/Button.tsx b/kafka-ui-react-app/src/components/common/Button/Button.tsx index 66cf42d347..5e0a6609eb 100644 --- a/kafka-ui-react-app/src/components/common/Button/Button.tsx +++ b/kafka-ui-react-app/src/components/common/Button/Button.tsx @@ -14,9 +14,11 @@ export const Button: React.FC = ({ to, ...props }) => { if (to) { return ( - {props.children} + + {props.children} + ); } - return ; + return ; }; diff --git a/kafka-ui-react-app/src/components/common/Checkbox/Checkbox.tsx b/kafka-ui-react-app/src/components/common/Checkbox/Checkbox.tsx new file mode 100644 index 0000000000..279872779e --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Checkbox/Checkbox.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { useFormContext } from 'react-hook-form'; +import { InputLabel } from 'components/common/Input/InputLabel.styled'; +import { FormError, InputHint } from 'components/common/Input/Input.styled'; +import { ErrorMessage } from '@hookform/error-message'; + +interface CheckboxProps { + name: string; + label: React.ReactNode; + hint?: string; +} + +const Checkbox: React.FC = ({ name, label, hint }) => { + const { register } = useFormContext(); + + return ( +
+ + + {label} + + {hint} + + + +
+ ); +}; + +export default Checkbox; diff --git a/kafka-ui-react-app/src/components/common/Form/Form.styled.ts b/kafka-ui-react-app/src/components/common/Form/Form.styled.ts index c72df5eb54..b9a598040c 100644 --- a/kafka-ui-react-app/src/components/common/Form/Form.styled.ts +++ b/kafka-ui-react-app/src/components/common/Form/Form.styled.ts @@ -3,4 +3,26 @@ import styled from 'styled-components'; export const StyledForm = styled.form` padding: 16px; max-width: 800px; + display: flex; + gap: 16px; + flex-direction: column; + + h3 { + margin-bottom: 0; + line-height: 32px; + } +`; + +export const FlexFieldset = styled.fieldset` + display: flex; + gap: 16px; + flex-direction: column; + + &:disabled { + ul { + opacity: 0.5; + background-color: #f5f5f5; + pointer-events: none; + } + } `; diff --git a/kafka-ui-react-app/src/components/common/Icons/ChevronDownIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/ChevronDownIcon.tsx new file mode 100644 index 0000000000..d9bf102474 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/ChevronDownIcon.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useTheme } from 'styled-components'; + +const ChevronDownIcon: React.FC = () => { + const theme = useTheme(); + return ( + + + + ); +}; + +export default ChevronDownIcon; diff --git a/kafka-ui-react-app/src/components/common/Input/Input.styled.ts b/kafka-ui-react-app/src/components/common/Input/Input.styled.ts index f1c91e3a53..52a4bc6fb3 100644 --- a/kafka-ui-react-app/src/components/common/Input/Input.styled.ts +++ b/kafka-ui-react-app/src/components/common/Input/Input.styled.ts @@ -75,3 +75,9 @@ export const FormError = styled.p` color: ${({ theme }) => theme.input.error}; font-size: 12px; `; + +export const InputHint = styled.p` + font-size: 0.85rem; + margin-top: 0.25rem; + color: ${({ theme }) => theme.text.secondary}; +`; diff --git a/kafka-ui-react-app/src/components/common/Input/Input.tsx b/kafka-ui-react-app/src/components/common/Input/Input.tsx index 932f954aa5..ae76bc4717 100644 --- a/kafka-ui-react-app/src/components/common/Input/Input.tsx +++ b/kafka-ui-react-app/src/components/common/Input/Input.tsx @@ -1,8 +1,10 @@ import React from 'react'; import { RegisterOptions, useFormContext } from 'react-hook-form'; import SearchIcon from 'components/common/Icons/SearchIcon'; +import { ErrorMessage } from '@hookform/error-message'; import * as S from './Input.styled'; +import { InputLabel } from './InputLabel.styled'; export interface InputProps extends React.InputHTMLAttributes, @@ -11,6 +13,90 @@ export interface InputProps hookFormOptions?: RegisterOptions; search?: boolean; positiveOnly?: boolean; + withError?: boolean; + label?: React.ReactNode; + hint?: React.ReactNode; + + // Some may only accept integer, like `Number of Partitions` + // some may accept decimal + integerOnly?: boolean; +} + +function inputNumberCheck( + key: string, + positiveOnly: boolean, + integerOnly: boolean, + getValues: (name: string) => string, + componentName: string +) { + let isValid = true; + if (!((key >= '0' && key <= '9') || key === '-' || key === '.')) { + // If not a valid digit char. + isValid = false; + } else { + // If there is any restriction. + if (positiveOnly) { + isValid = !(key === '-'); + } + if (isValid && integerOnly) { + isValid = !(key === '.'); + } + + // Check invalid format + const value = getValues(componentName); + + if (isValid && (key === '-' || key === '.')) { + if (!positiveOnly) { + if (key === '-') { + if (value !== '') { + // '-' should not appear anywhere except the start of the string + isValid = false; + } + } + } + if (!integerOnly) { + if (key === '.') { + if (value === '' || value.indexOf('.') !== -1) { + // '.' should not appear at the start of the string or appear twice + isValid = false; + } + } + } + } + } + return isValid; +} + +function pasteNumberCheck( + text: string, + positiveOnly: boolean, + integerOnly: boolean +) { + let value: string; + value = text; + let sign = ''; + if (!positiveOnly) { + if (value.charAt(0) === '-') { + sign = '-'; + } + } + if (integerOnly) { + value = value.replace(/\D/g, ''); + } else { + value = value.replace(/[^\d.]/g, ''); + if (value.indexOf('.') !== value.lastIndexOf('.')) { + const strs = value.split('.'); + value = ''; + for (let i = 0; i < strs.length; i += 1) { + value += strs[i]; + if (i === 0) { + value += '.'; + } + } + } + } + value = sign + value; + return value; } const Input: React.FC = ({ @@ -20,17 +106,34 @@ const Input: React.FC = ({ inputSize = 'L', type, positiveOnly, + integerOnly, + withError = false, + label, + hint, ...rest }) => { const methods = useFormContext(); + + const fieldId = React.useId(); + + const isHookFormField = !!name && !!methods.register; + const keyPressEventHandler = ( event: React.KeyboardEvent ) => { - const { key, code } = event; + const { key } = event; if (type === 'number') { - // Manualy prevent input of 'e' character for all number inputs + // Manually prevent input of non-digit and non-minus for all number inputs // and prevent input of negative numbers for positiveOnly inputs - if (key === 'e' || (positiveOnly && (key === '-' || code === 'Minus'))) { + if ( + !inputNumberCheck( + key, + typeof positiveOnly === 'boolean' ? positiveOnly : false, + typeof integerOnly === 'boolean' ? integerOnly : false, + methods.getValues, + typeof name === 'string' ? name : '' + ) + ) { event.preventDefault(); } } @@ -38,24 +141,21 @@ const Input: React.FC = ({ const pasteEventHandler = (event: React.ClipboardEvent) => { if (type === 'number') { const { clipboardData } = event; - const text = clipboardData.getData('Text'); - // replace all non-digit characters with empty string - let value = text.replace(/[^\d.]/g, ''); - if (positiveOnly) { - // check if value is negative - const parsedData = parseFloat(value); - if (parsedData < 0) { - // remove minus sign - value = String(Math.abs(parsedData)); - } - } + // The 'clipboardData' does not have key 'Text', but has key 'text' instead. + const text = clipboardData.getData('text'); + // Check the format of pasted text. + const value = pasteNumberCheck( + text, + typeof positiveOnly === 'boolean' ? positiveOnly : false, + typeof integerOnly === 'boolean' ? integerOnly : false + ); // if paste value contains non-numeric characters or // negative for positiveOnly fields then prevent paste if (value !== text) { event.preventDefault(); // for react-hook-form fields only set transformed value - if (name) { + if (isHookFormField) { methods.setValue(name, value); } } @@ -63,24 +163,34 @@ const Input: React.FC = ({ }; let inputOptions = { ...rest }; - if (name) { + if (isHookFormField) { // extend input options with react-hook-form options // if the field is a part of react-hook-form form inputOptions = { ...rest, ...methods.register(name, hookFormOptions) }; } return ( - - {search && } - - +
+ {label && {label}} + + {search && } + + {withError && isHookFormField && ( + + + + )} + {hint && {hint}} + +
); }; diff --git a/kafka-ui-react-app/src/components/common/Input/InputLabel.styled.ts b/kafka-ui-react-app/src/components/common/Input/InputLabel.styled.ts index 646beabc10..a33fb1f763 100644 --- a/kafka-ui-react-app/src/components/common/Input/InputLabel.styled.ts +++ b/kafka-ui-react-app/src/components/common/Input/InputLabel.styled.ts @@ -5,7 +5,6 @@ export const InputLabel = styled.label` font-size: 12px; line-height: 20px; color: ${({ theme }) => theme.input.label.color}; - input[type='checkbox'] { display: inline-block; margin-right: 8px; diff --git a/kafka-ui-react-app/src/components/common/Input/__tests__/Input.spec.tsx b/kafka-ui-react-app/src/components/common/Input/__tests__/Input.spec.tsx index 3082f6f610..0254196965 100644 --- a/kafka-ui-react-app/src/components/common/Input/__tests__/Input.spec.tsx +++ b/kafka-ui-react-app/src/components/common/Input/__tests__/Input.spec.tsx @@ -4,12 +4,23 @@ import { screen } from '@testing-library/react'; import { render } from 'lib/testHelpers'; import userEvent from '@testing-library/user-event'; +// Mock useFormContext +let component: HTMLInputElement; + const setupWrapper = (props?: Partial) => ( ); jest.mock('react-hook-form', () => ({ useFormContext: () => ({ register: jest.fn(), + + // Mock methods.getValues and methods.setValue + getValues: jest.fn(() => { + return component.value; + }), + setValue: jest.fn((key, val) => { + component.value = val; + }), }), })); @@ -23,20 +34,146 @@ describe('Custom Input', () => { }); }); describe('number', () => { - const getInput = () => screen.getByRole('spinbutton'); + const getInput = () => screen.getByRole('spinbutton'); - it('allows user to type only numbers', async () => { - render(setupWrapper({ type: 'number' })); - const input = getInput(); - await userEvent.type(input, 'abc131'); - expect(input).toHaveValue(131); + describe('input', () => { + it('allows user to type numbers only', async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.type(input, 'abc131'); + expect(input).toHaveValue(131); + }); + + it('allows user to type negative values', async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.type(input, '-2'); + expect(input).toHaveValue(-2); + }); + + it('allows user to type positive values only', async () => { + render(setupWrapper({ type: 'number', positiveOnly: true })); + const input = getInput(); + component = input; + await userEvent.type(input, '-2'); + expect(input).toHaveValue(2); + }); + + it('allows user to type decimal', async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.type(input, '2.3'); + expect(input).toHaveValue(2.3); + }); + + it('allows user to type integer only', async () => { + render(setupWrapper({ type: 'number', integerOnly: true })); + const input = getInput(); + component = input; + await userEvent.type(input, '2.3'); + expect(input).toHaveValue(23); + }); + + it("not allow '-' appear at any position of the string except the start", async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.type(input, '2-3'); + expect(input).toHaveValue(23); + }); + + it("not allow '.' appear at the start of the string", async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.type(input, '.33'); + expect(input).toHaveValue(33); + }); + + it("not allow '.' appear twice in the string", async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.type(input, '3.3.3'); + expect(input).toHaveValue(3.33); + }); }); - it('allows negative values', async () => { - render(setupWrapper({ type: 'number' })); - const input = getInput(); - await userEvent.type(input, '-2'); - expect(input).toHaveValue(-2); + describe('paste', () => { + it('allows user to paste numbers only', async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('abc131'); + expect(input).toHaveValue(131); + }); + + it('allows user to paste negative values', async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('-2'); + expect(input).toHaveValue(-2); + }); + + it('allows user to paste positive values only', async () => { + render(setupWrapper({ type: 'number', positiveOnly: true })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('-2'); + expect(input).toHaveValue(2); + }); + + it('allows user to paste decimal', async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('2.3'); + expect(input).toHaveValue(2.3); + }); + + it('allows user to paste integer only', async () => { + render(setupWrapper({ type: 'number', integerOnly: true })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('2.3'); + expect(input).toHaveValue(23); + }); + + it("not allow '-' appear at any position of the pasted string except the start", async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('2-3'); + expect(input).toHaveValue(23); + }); + + it("not allow '.' appear at the start of the pasted string", async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('.33'); + expect(input).toHaveValue(0.33); + }); + + it("not allow '.' appear twice in the pasted string", async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('3.3.3'); + expect(input).toHaveValue(3.33); + }); }); }); }); diff --git a/kafka-ui-react-app/src/components/common/NewTable/Table.tsx b/kafka-ui-react-app/src/components/common/NewTable/Table.tsx index 07cf338f10..fb6f76aa39 100644 --- a/kafka-ui-react-app/src/components/common/NewTable/Table.tsx +++ b/kafka-ui-react-app/src/components/common/NewTable/Table.tsx @@ -46,7 +46,7 @@ export interface TableProps { enableSorting?: boolean; // Enables sorting for table. // Placeholder for empty table - emptyMessage?: string; + emptyMessage?: React.ReactNode; // Handles row click. Can not be combined with `enableRowSelection` && expandable rows. onRowClick?: (row: Row) => void; diff --git a/kafka-ui-react-app/src/components/common/NewTable/__test__/Table.spec.tsx b/kafka-ui-react-app/src/components/common/NewTable/__test__/Table.spec.tsx index 43dd7b8dbf..e31a88846d 100644 --- a/kafka-ui-react-app/src/components/common/NewTable/__test__/Table.spec.tsx +++ b/kafka-ui-react-app/src/components/common/NewTable/__test__/Table.spec.tsx @@ -20,7 +20,18 @@ jest.mock('react-router-dom', () => ({ useNavigate: () => mockedUsedNavigate, })); -type Datum = typeof data[0]; +// This is needed by ESLint. +jest.mock('react-hook-form', () => ({ + useFormContext: () => ({ + register: jest.fn(), + + // Mock methods.getValues and methods.setValue + getValues: jest.fn(), + setValue: jest.fn(), + }), +})); + +type Datum = (typeof data)[0]; const data = [ { diff --git a/kafka-ui-react-app/src/components/common/Select/ControlledSelect.tsx b/kafka-ui-react-app/src/components/common/Select/ControlledSelect.tsx new file mode 100644 index 0000000000..b3e15aacdf --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Select/ControlledSelect.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { Controller } from 'react-hook-form'; +import { FormError } from 'components/common/Input/Input.styled'; +import { InputLabel } from 'components/common/Input/InputLabel.styled'; +import { ErrorMessage } from '@hookform/error-message'; + +import Select, { SelectOption } from './Select'; + +interface ControlledSelectProps { + name: string; + label: React.ReactNode; + hint?: string; + options: SelectOption[]; + onChange?: (val: string | number) => void; + disabled?: boolean; + placeholder?: string; +} + +const ControlledSelect: React.FC = ({ + name, + label, + onChange, + options, + disabled = false, + placeholder, +}) => { + const id = React.useId(); + + return ( +
+ {label} + { + return ( + + + + ); + case 'SASL/GSSAPI': + return ( + <> + + + + + + ); + case 'SASL/OAUTHBEARER': + return ( + + ); + case 'SASL/PLAIN': + case 'SASL/SCRAM-256': + case 'SASL/SCRAM-512': + case 'SASL/LDAP': + return ; + case 'Delegation tokens': + return ( + <> + + + + ); + case 'SASL/AWS IAM': + return ( + + ); + case 'mTLS': + return ; + default: + return null; + } +}; + +export default AuthenticationMethods; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/CustomAuthentication.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/CustomAuthentication.tsx new file mode 100644 index 0000000000..c751b9c56d --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/CustomAuthentication.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { useFormContext } from 'react-hook-form'; +import Input from 'components/common/Input/Input'; +import { convertFormKeyToPropsKey } from 'widgets/ClusterConfigForm/utils/convertFormKeyToPropsKey'; +import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader'; + +const CustomAuthentication: React.FC = () => { + const { watch, setValue } = useFormContext(); + const customConf = watch('customAuth'); + const hasCustomConfig = + customConf && Object.values(customConf).some((v) => !!v); + + const remove = () => + setValue('customAuth', undefined, { + shouldValidate: true, + shouldDirty: true, + shouldTouch: true, + }); + return ( + <> + + {hasCustomConfig && ( + <> + {Object.keys(customConf).map((key) => ( + + ))} + + )} + + ); +}; + +export default CustomAuthentication; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KSQL.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KSQL.tsx new file mode 100644 index 0000000000..8d28ad65c6 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KSQL.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import Input from 'components/common/Input/Input'; +import { useFormContext } from 'react-hook-form'; +import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader'; +import SSLForm from 'widgets/ClusterConfigForm/common/SSLForm'; +import Credentials from 'widgets/ClusterConfigForm/common/Credentials'; + +const KSQL = () => { + const { setValue, watch } = useFormContext(); + const ksql = watch('ksql'); + const toggleConfig = () => { + setValue('ksql', ksql ? undefined : { url: '', isAuth: false }, { + shouldValidate: true, + shouldDirty: true, + shouldTouch: true, + }); + }; + return ( + <> + + {ksql && ( + <> + + + + + )} + + ); +}; +export default KSQL; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KafkaCluster.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KafkaCluster.tsx new file mode 100644 index 0000000000..c71f2d72ec --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KafkaCluster.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import Input from 'components/common/Input/Input'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { FormError, InputHint } from 'components/common/Input/Input.styled'; +import { ErrorMessage } from '@hookform/error-message'; +import CloseIcon from 'components/common/Icons/CloseIcon'; +import { Button } from 'components/common/Button/Button'; +import PlusIcon from 'components/common/Icons/PlusIcon'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; +import Heading from 'components/common/heading/Heading.styled'; +import { InputLabel } from 'components/common/Input/InputLabel.styled'; +import Checkbox from 'components/common/Checkbox/Checkbox'; +import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader'; +import SSLForm from 'widgets/ClusterConfigForm/common/SSLForm'; + +const KafkaCluster: React.FC = () => { + const { control, watch, setValue } = useFormContext(); + + const { fields, append, remove } = useFieldArray({ + control, + name: 'bootstrapServers', + }); + + const hasTrustStore = !!watch('truststore'); + + const toggleSection = (section: string) => () => + setValue( + section, + watch(section) + ? undefined + : { + location: '', + password: '', + }, + { shouldValidate: true, shouldDirty: true, shouldTouch: true } + ); + + return ( + <> + Kafka Cluster + + +
+ Bootstrap Servers * + + the list of Kafka brokers that you want to connect to + + + {fields.map((field, index) => ( + +
+ +
+
+ +
+ remove(index)} + > + + +
+ ))} + + + +
+ +
+
+
+
+ + {hasTrustStore && } + + ); +}; +export default KafkaCluster; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KafkaConnect.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KafkaConnect.tsx new file mode 100644 index 0000000000..4f5840ff23 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KafkaConnect.tsx @@ -0,0 +1,91 @@ +import * as React from 'react'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; +import { Button } from 'components/common/Button/Button'; +import Input from 'components/common/Input/Input'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import PlusIcon from 'components/common/Icons/PlusIcon'; +import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper'; +import CloseIcon from 'components/common/Icons/CloseIcon'; +import { + FlexGrow1, + FlexRow, +} from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; +import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader'; +import Credentials from 'widgets/ClusterConfigForm/common/Credentials'; +import SSLForm from 'widgets/ClusterConfigForm/common/SSLForm'; + +const KafkaConnect = () => { + const { control } = useFormContext(); + const { fields, append, remove } = useFieldArray({ + control, + name: 'kafkaConnect', + }); + const handleAppend = () => append({ name: '', address: '' }); + const toggleConfig = () => (fields.length === 0 ? handleAppend() : remove()); + + const hasFields = fields.length > 0; + + return ( + <> + + {hasFields && ( + + {fields.map((item, index) => ( +
+ + + + + + + + remove(index)}> + + + + + + +
+
+ ))} + +
+ )} + + ); +}; +export default KafkaConnect; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/Metrics.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/Metrics.tsx new file mode 100644 index 0000000000..e9d67f2014 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/Metrics.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import Input from 'components/common/Input/Input'; +import { useFormContext } from 'react-hook-form'; +import ControlledSelect from 'components/common/Select/ControlledSelect'; +import { METRICS_OPTIONS } from 'lib/constants'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; +import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader'; +import SSLForm from 'widgets/ClusterConfigForm/common/SSLForm'; +import Credentials from 'widgets/ClusterConfigForm/common/Credentials'; + +const Metrics = () => { + const { setValue, watch } = useFormContext(); + const visibleMetrics = !!watch('metrics'); + const toggleMetrics = () => + setValue( + 'metrics', + visibleMetrics + ? undefined + : { + type: '', + port: 0, + isAuth: false, + }, + { shouldValidate: true, shouldDirty: true, shouldTouch: true } + ); + + return ( + <> + + {visibleMetrics && ( + <> + + + + + + + + )} + + ); +}; +export default Metrics; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/SchemaRegistry.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/SchemaRegistry.tsx new file mode 100644 index 0000000000..bd36ee8934 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/SchemaRegistry.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import Input from 'components/common/Input/Input'; +import { useFormContext } from 'react-hook-form'; +import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader'; +import SSLForm from 'widgets/ClusterConfigForm/common/SSLForm'; +import Credentials from 'widgets/ClusterConfigForm/common/Credentials'; + +const SchemaRegistry = () => { + const { setValue, watch } = useFormContext(); + const schemaRegistry = watch('schemaRegistry'); + const toggleConfig = () => { + setValue( + 'schemaRegistry', + schemaRegistry ? undefined : { url: '', isAuth: false }, + { shouldValidate: true, shouldDirty: true, shouldTouch: true } + ); + }; + return ( + <> + + {schemaRegistry && ( + <> + + + + + )} + + ); +}; +export default SchemaRegistry; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/Credentials.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/Credentials.tsx new file mode 100644 index 0000000000..f991b11096 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/Credentials.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import Input from 'components/common/Input/Input'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; +import Checkbox from 'components/common/Checkbox/Checkbox'; +import { useFormContext } from 'react-hook-form'; + +type CredentialsProps = { + prefix: string; + title?: string; +}; + +const Credentials: React.FC = ({ + prefix, + title = 'Secured with auth?', +}) => { + const { watch } = useFormContext(); + + return ( + + + {watch(`${prefix}.isAuth`) && ( + + + + + + + + + )} + + ); +}; + +export default Credentials; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/Fileupload.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/Fileupload.tsx new file mode 100644 index 0000000000..4a6244ada0 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/Fileupload.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import { FormError } from 'components/common/Input/Input.styled'; +import { InputLabel } from 'components/common/Input/InputLabel.styled'; +import { ErrorMessage } from '@hookform/error-message'; +import { useFormContext } from 'react-hook-form'; +import Input from 'components/common/Input/Input'; +import { Button } from 'components/common/Button/Button'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; +import { useAppConfigFilesUpload } from 'lib/hooks/api/appConfig'; + +const Fileupload: React.FC<{ name: string; label: string }> = ({ + name, + label, +}) => { + const upload = useAppConfigFilesUpload(); + + const id = React.useId(); + const { watch, setValue } = useFormContext(); + const loc = watch(name); + + const handleFileChange = async (e: React.ChangeEvent) => { + if (e.target.files) { + const formData = new FormData(); + const file = e.target.files[0]; + formData.append('file', file); + const resp = await upload.mutateAsync(formData); + setValue(name, resp.location, { + shouldValidate: true, + shouldDirty: true, + }); + } + }; + + const onReset = () => { + setValue(name, '', { shouldValidate: true, shouldDirty: true }); + }; + + return ( +
+ {label} + + {loc ? ( + + + + + + + ) : ( + + {upload.isLoading ? ( +

Uploading...

+ ) : ( + + )} +
+ )} + + + +
+ ); +}; + +export default Fileupload; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/SSLForm.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/SSLForm.tsx new file mode 100644 index 0000000000..98877a3d29 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/SSLForm.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import Input from 'components/common/Input/Input'; +import Fileupload from 'widgets/ClusterConfigForm/common/Fileupload'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; + +type SSLFormProps = { + prefix: string; + title: string; +}; + +const SSLForm: React.FC = ({ prefix, title }) => { + return ( + + + + + ); +}; + +export default SSLForm; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/SectionHeader.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/SectionHeader.tsx new file mode 100644 index 0000000000..1265459b5c --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/SectionHeader.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { Button } from 'components/common/Button/Button'; +import Heading from 'components/common/heading/Heading.styled'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; + +interface SectionHeaderProps { + title: string; + addButtonText: string; + adding?: boolean; + onClick: () => void; +} + +const SectionHeader: React.FC = ({ + adding, + title, + addButtonText, + onClick, +}) => { + return ( + + + {title} + + + + ); +}; + +export default SectionHeader; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/index.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/index.tsx new file mode 100644 index 0000000000..8bd0c3f125 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/index.tsx @@ -0,0 +1,155 @@ +import React from 'react'; +import { Button } from 'components/common/Button/Button'; +import { useForm, FormProvider } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import formSchema from 'widgets/ClusterConfigForm/schema'; +import { FlexFieldset, StyledForm } from 'components/common/Form/Form.styled'; +import { + useUpdateAppConfig, + useValidateAppConfig, +} from 'lib/hooks/api/appConfig'; +import { ClusterConfigFormValues } from 'widgets/ClusterConfigForm/types'; +import { transformFormDataToPayload } from 'widgets/ClusterConfigForm/utils/transformFormDataToPayload'; +import { showAlert, showSuccessAlert } from 'lib/errorHandling'; +import { getIsValidConfig } from 'widgets/ClusterConfigForm/utils/getIsValidConfig'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; +import { useNavigate } from 'react-router-dom'; +import useBoolean from 'lib/hooks/useBoolean'; +import KafkaCluster from 'widgets/ClusterConfigForm/Sections/KafkaCluster'; +import SchemaRegistry from 'widgets/ClusterConfigForm/Sections/SchemaRegistry'; +import KafkaConnect from 'widgets/ClusterConfigForm/Sections/KafkaConnect'; +import Metrics from 'widgets/ClusterConfigForm/Sections/Metrics'; +import CustomAuthentication from 'widgets/ClusterConfigForm/Sections/CustomAuthentication'; +import Authentication from 'widgets/ClusterConfigForm/Sections/Authentication/Authentication'; +import KSQL from 'widgets/ClusterConfigForm/Sections/KSQL'; + +interface ClusterConfigFormProps { + hasCustomConfig?: boolean; + initialValues?: Partial; +} + +const CLUSTER_CONFIG_FORM_DEFAULT_VALUES: Partial = { + bootstrapServers: [{ host: '', port: '' }], +}; + +const ClusterConfigForm: React.FC = ({ + initialValues = {}, + hasCustomConfig, +}) => { + const navigate = useNavigate(); + const methods = useForm({ + mode: 'all', + resolver: yupResolver(formSchema), + defaultValues: { + ...CLUSTER_CONFIG_FORM_DEFAULT_VALUES, + ...initialValues, + }, + }); + const { + formState: { isSubmitting, isDirty }, + trigger, + } = methods; + + const validate = useValidateAppConfig(); + const update = useUpdateAppConfig({ initialName: initialValues.name }); + const { + value: isFormDisabled, + setTrue: disableForm, + setFalse: enableForm, + } = useBoolean(); + + const onSubmit = async (data: ClusterConfigFormValues) => { + const config = transformFormDataToPayload(data); + try { + await update.mutateAsync(config); + navigate('/'); + } catch (e) { + showAlert('error', { + id: 'app-config-update-error', + title: 'Error updating application config', + message: 'There was an error updating the application config', + }); + } + }; + + const onReset = () => methods.reset(); + + const onValidate = async () => { + await trigger(); + if (!methods.formState.isValid) return; + disableForm(); + const data = methods.getValues(); + const config = transformFormDataToPayload(data); + + try { + const response = await validate.mutateAsync(config); + const isConfigValid = getIsValidConfig(response, data.name); + if (isConfigValid) { + showSuccessAlert({ + message: 'Configuration is valid', + }); + } + } catch (e) { + showAlert('error', { + id: 'app-config-validate-error', + title: 'Error validating application config', + message: 'There was an error validating the application config', + }); + } + enableForm(); + }; + + const showCustomConfig = methods.watch('customAuth') && hasCustomConfig; + + const isValidateDisabled = isSubmitting; + const isSubmitDisabled = isSubmitting || !isDirty; + + return ( + + + + +
+ {showCustomConfig ? : } +
+ +
+ +
+ +
+ +
+ + + + + +
+
+
+ ); +}; + +export default ClusterConfigForm; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/schema.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/schema.ts new file mode 100644 index 0000000000..abd17f34bf --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/schema.ts @@ -0,0 +1,194 @@ +import { isArray } from 'lodash'; +import { object, string, number, array, boolean, mixed, lazy } from 'yup'; + +const requiredString = string().required('required field'); + +const portSchema = number() + .positive('positive only') + .typeError('numbers only') + .required('required'); + +const bootstrapServerSchema = object({ + host: requiredString, + port: portSchema, +}); + +const sslSchema = lazy((value) => { + if (typeof value === 'object') { + return object({ + location: string().when('password', { + is: (v: string) => !!v, + then: (schema) => schema.required('required field'), + }), + password: string(), + }); + } + return mixed().optional(); +}); + +const urlWithAuthSchema = lazy((value) => { + if (typeof value === 'object') { + return object({ + url: requiredString, + isAuth: boolean(), + username: string().when('isAuth', { + is: true, + then: (schema) => schema.required('required field'), + }), + password: string().when('isAuth', { + is: true, + then: (schema) => schema.required('required field'), + }), + keystore: sslSchema, + }); + } + return mixed().optional(); +}); + +const kafkaConnectSchema = object({ + name: requiredString, + address: requiredString, + isAuth: boolean(), + username: string().when('isAuth', { + is: true, + then: (schema) => schema.required('required field'), + }), + password: string().when('isAuth', { + is: true, + then: (schema) => schema.required('required field'), + }), + keystore: sslSchema, +}); + +const kafkaConnectsSchema = lazy((value) => { + if (isArray(value)) { + return array().of(kafkaConnectSchema); + } + return mixed().optional(); +}); + +const metricsSchema = lazy((value) => { + if (typeof value === 'object') { + return object({ + type: string().oneOf(['JMX', 'PROMETHEUS']).required('required field'), + port: portSchema, + isAuth: boolean(), + username: string().when('isAuth', { + is: true, + then: (schema) => schema.required('required field'), + }), + password: string().when('isAuth', { + is: true, + then: (schema) => schema.required('required field'), + }), + keystore: sslSchema, + }); + } + return mixed().optional(); +}); + +const authPropsSchema = lazy((_, { parent }) => { + switch (parent.method) { + case 'SASL/JAAS': + return object({ + saslJaasConfig: requiredString, + saslMechanism: requiredString, + }); + case 'SASL/GSSAPI': + return object({ + saslKerberosServiceName: requiredString, + keyTabFile: string(), + storeKey: boolean(), + principal: requiredString, + }); + case 'SASL/OAUTHBEARER': + return object({ + unsecuredLoginStringClaim_sub: requiredString, + }); + case 'SASL/PLAIN': + case 'SASL/SCRAM-256': + case 'SASL/SCRAM-512': + case 'SASL/LDAP': + return object({ + username: requiredString, + password: requiredString, + }); + case 'Delegation tokens': + return object({ + tokenId: requiredString, + tokenValue: requiredString, + }); + case 'SASL/AWS IAM': + return object({ + awsProfileName: string(), + }); + case 'mTLS': + default: + return mixed().optional(); + } +}); + +const authSchema = lazy((value) => { + if (typeof value === 'object') { + return object({ + method: string() + .required('required field') + .oneOf([ + 'SASL/JAAS', + 'SASL/GSSAPI', + 'SASL/OAUTHBEARER', + 'SASL/PLAIN', + 'SASL/SCRAM-256', + 'SASL/SCRAM-512', + 'Delegation tokens', + 'SASL/LDAP', + 'SASL/AWS IAM', + 'mTLS', + ]), + securityProtocol: string() + .oneOf(['SASL_SSL', 'SASL_PLAINTEXT']) + .when('method', { + is: (v: string) => { + return [ + 'SASL/JAAS', + 'SASL/GSSAPI', + 'SASL/OAUTHBEARER', + 'SASL/PLAIN', + 'SASL/SCRAM-256', + 'SASL/SCRAM-512', + 'SASL/LDAP', + 'SASL/AWS IAM', + ].includes(v); + }, + then: (schema) => schema.required('required field'), + }), + keystore: lazy((_, { parent }) => { + if (parent.method === 'mTLS') { + return object({ + location: requiredString, + password: string(), + }); + } + return mixed().optional(); + }), + props: authPropsSchema, + }); + } + return mixed().optional(); +}); + +const formSchema = object({ + name: string() + .required('required field') + .min(3, 'Cluster name must be at least 3 characters'), + readOnly: boolean().required('required field'), + bootstrapServers: array().of(bootstrapServerSchema).min(1), + truststore: sslSchema, + auth: authSchema, + schemaRegistry: urlWithAuthSchema, + ksql: urlWithAuthSchema, + kafkaConnect: kafkaConnectsSchema, + metrics: metricsSchema, +}); + +export default formSchema; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/types.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/types.ts new file mode 100644 index 0000000000..6af74b5f84 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/types.ts @@ -0,0 +1,56 @@ +type SecurityProtocol = 'SASL_SSL' | 'SASL_PLAINTEXT'; +type BootstrapServer = { + host: string; + port: string; +}; + +type WithKeystore = { + keystore?: { + location: string; + password: string; + }; +}; + +type WithAuth = { + isAuth: boolean; + username?: string; + password?: string; +}; + +type URLWithAuth = WithAuth & + WithKeystore & { + url?: string; + }; + +type KafkaConnect = WithAuth & + WithKeystore & { + name: string; + address: string; + }; + +type Metrics = WithAuth & + WithKeystore & { + type: string; + port: string; + }; + +export type ClusterConfigFormValues = { + name: string; + readOnly: boolean; + bootstrapServers: BootstrapServer[]; + truststore?: { + location: string; + password: string; + }; + auth?: WithKeystore & { + method: string; + securityProtocol: SecurityProtocol; + props: Record; + }; + schemaRegistry?: URLWithAuth; + ksql?: URLWithAuth; + properties?: Record; + kafkaConnect?: KafkaConnect[]; + metrics?: Metrics; + customAuth: Record; +}; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/convertFormKeyToPropsKey.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/convertFormKeyToPropsKey.ts new file mode 100644 index 0000000000..1759b3b5f6 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/convertFormKeyToPropsKey.ts @@ -0,0 +1,3 @@ +export const convertFormKeyToPropsKey = (key: string) => { + return key.split('___').join('.'); +}; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/convertPropsKeyToFormKey.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/convertPropsKeyToFormKey.ts new file mode 100644 index 0000000000..37312be745 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/convertPropsKeyToFormKey.ts @@ -0,0 +1,3 @@ +export const convertPropsKeyToFormKey = (key: string) => { + return key.split('.').join('___'); +}; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts new file mode 100644 index 0000000000..73a945818d --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts @@ -0,0 +1,121 @@ +import { + ApplicationConfigPropertiesKafkaClusters, + ApplicationConfigPropertiesKafkaSchemaRegistrySsl, +} from 'generated-sources'; +import { ClusterConfigFormValues } from 'widgets/ClusterConfigForm/types'; + +import { convertPropsKeyToFormKey } from './convertPropsKeyToFormKey'; + +const parseBootstrapServers = (bootstrapServers?: string) => + bootstrapServers?.split(',').map((url) => { + const [host, port] = url.split(':'); + return { host, port }; + }); + +const parseKeystore = ( + keystore?: ApplicationConfigPropertiesKafkaSchemaRegistrySsl +) => { + if (!keystore) return undefined; + const { keystoreLocation, keystorePassword } = keystore; + return { + keystore: { + location: keystoreLocation as string, + password: keystorePassword as string, + }, + }; +}; + +const parseCredentials = (username?: string, password?: string) => { + if (!username || !password) return { isAuth: false }; + return { isAuth: true, username, password }; +}; + +export const getInitialFormData = ( + payload: ApplicationConfigPropertiesKafkaClusters +) => { + const { + ssl, + schemaRegistry, + schemaRegistryAuth, + schemaRegistrySsl, + kafkaConnect, + metrics, + ksqldbServer, + ksqldbServerAuth, + ksqldbServerSsl, + } = payload; + + const initialValues: Partial = { + name: payload.name as string, + readOnly: !!payload.readOnly, + bootstrapServers: parseBootstrapServers(payload.bootstrapServers), + }; + + const { truststoreLocation, truststorePassword } = ssl || {}; + + if (truststoreLocation && truststorePassword) { + initialValues.truststore = { + location: truststoreLocation, + password: truststorePassword, + }; + } + + if (schemaRegistry) { + initialValues.schemaRegistry = { + url: schemaRegistry, + ...parseCredentials( + schemaRegistryAuth?.username, + schemaRegistryAuth?.password + ), + ...parseKeystore(schemaRegistrySsl), + }; + } + if (ksqldbServer) { + initialValues.ksql = { + url: ksqldbServer, + ...parseCredentials( + ksqldbServerAuth?.username, + ksqldbServerAuth?.password + ), + ...parseKeystore(ksqldbServerSsl), + }; + } + + if (kafkaConnect && kafkaConnect.length > 0) { + initialValues.kafkaConnect = kafkaConnect.map((c) => ({ + name: c.name as string, + address: c.address as string, + ...parseCredentials(c.username, c.password), + ...parseKeystore(c), + })); + } + + if (metrics) { + initialValues.metrics = { + type: metrics.type as string, + ...parseCredentials(metrics.username, metrics.password), + ...parseKeystore(metrics), + port: `${metrics.port}`, + }; + } + + const properties = payload.properties || {}; + + // Authentification + initialValues.customAuth = {}; + + Object.entries(properties).forEach(([key, val]) => { + if ( + key.startsWith('security.') || + key.startsWith('sasl.') || + key.startsWith('ssl.') + ) { + initialValues.customAuth = { + ...initialValues.customAuth, + [convertPropsKeyToFormKey(key)]: val, + }; + } + }); + + return initialValues as ClusterConfigFormValues; +}; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getIsValidConfig.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getIsValidConfig.ts new file mode 100644 index 0000000000..d39f1aebff --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getIsValidConfig.ts @@ -0,0 +1,49 @@ +import { ApplicationConfigValidation } from 'generated-sources'; +import { showAlert } from 'lib/errorHandling'; + +export const getIsValidConfig = ( + { clusters }: ApplicationConfigValidation, + name: string +) => { + let isValid = true; + const prefix = `cluster-${name}`; + const clusterErrors = clusters?.[name]; + + if (clusterErrors?.kafka?.error) { + isValid = false; + showAlert('error', { + id: `${prefix}-kafka`, + title: 'Kafka Cluster', + message: clusterErrors?.kafka.errorMessage, + }); + } + if (clusterErrors?.schemaRegistry?.error) { + isValid = false; + showAlert('error', { + id: `${prefix}-schemaRegistry`, + title: 'Schema Registry', + message: clusterErrors?.schemaRegistry.errorMessage, + }); + } + if (clusterErrors?.ksqldb?.error) { + isValid = false; + showAlert('error', { + id: `${prefix}-ksqldb`, + title: 'KSQL DB', + message: clusterErrors?.ksqldb?.errorMessage, + }); + } + if (clusterErrors?.kafkaConnects) { + Object.entries(clusterErrors.kafkaConnects).forEach(([key, val]) => { + if (val?.error) { + isValid = false; + showAlert('error', { + id: `${prefix}-kafkaConnects-${key}`, + title: `Kafka Connect. ${key}`, + message: val.errorMessage, + }); + } + }); + } + return isValid; +}; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getJaasConfig.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getJaasConfig.ts new file mode 100644 index 0000000000..065612451f --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getJaasConfig.ts @@ -0,0 +1,27 @@ +import { isUndefined } from 'lodash'; + +const JAAS_CONFIGS = { + 'SASL/GSSAPI': 'com.sun.security.auth.module.Krb5LoginModule', + 'SASL/OAUTHBEARER': + 'org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule', + 'SASL/PLAIN': 'org.apache.kafka.common.security.plain.PlainLoginModule', + 'SASL/SCRAM-256': 'org.apache.kafka.common.security.scram.ScramLoginModule', + 'SASL/SCRAM-512': 'org.apache.kafka.common.security.scram.ScramLoginModule', + 'Delegation tokens': + 'org.apache.kafka.common.security.scram.ScramLoginModule', + 'SASL/LDAP': 'org.apache.kafka.common.security.plain.PlainLoginModule', + 'SASL/AWS IAM': 'software.amazon.msk.auth.iam.IAMLoginModule', +}; + +type MethodName = keyof typeof JAAS_CONFIGS; + +export const getJaasConfig = ( + method: MethodName, + options: Record +) => { + const optionsString = Object.entries(options) + .map(([key, value]) => (isUndefined(value) ? null : ` ${key}="${value}"`)) + .join(''); + + return `${JAAS_CONFIGS[method]} required${optionsString};`; +}; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts new file mode 100644 index 0000000000..0a5d6c3523 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts @@ -0,0 +1,231 @@ +import { ClusterConfigFormValues } from 'widgets/ClusterConfigForm/types'; +import { ApplicationConfigPropertiesKafkaClusters } from 'generated-sources'; + +import { getJaasConfig } from './getJaasConfig'; +import { convertFormKeyToPropsKey } from './convertFormKeyToPropsKey'; + +const transformToKeystore = (keystore?: { + location: string; + password: string; +}) => { + if (!keystore || !keystore.location) return undefined; + return { + keystoreLocation: keystore.location, + keystorePassword: keystore.password, + }; +}; + +const transformToCredentials = ( + isAuth: boolean, + username?: string, + password?: string +) => { + if (!isAuth || !username || !password) return undefined; + return { username, password }; +}; + +const transformCustomProps = (props: Record) => { + const config: Record = {}; + if (!props) return config; + + Object.entries(props).forEach(([key, val]) => { + if (props[key]) config[convertFormKeyToPropsKey(key)] = val; + }); + + return config; +}; + +export const transformFormDataToPayload = (data: ClusterConfigFormValues) => { + const config: ApplicationConfigPropertiesKafkaClusters = { + name: data.name, + bootstrapServers: data.bootstrapServers + .map(({ host, port }) => `${host}:${port}`) + .join(','), + readOnly: data.readOnly, + }; + + if (data.truststore) { + config.ssl = { + truststoreLocation: data.truststore?.location, + truststorePassword: data.truststore?.password, + }; + } + + // Schema Registry + if (data.schemaRegistry) { + config.schemaRegistry = data.schemaRegistry.url; + config.schemaRegistryAuth = transformToCredentials( + data.schemaRegistry.isAuth, + data.schemaRegistry.username, + data.schemaRegistry.password + ); + config.schemaRegistrySsl = transformToKeystore( + data.schemaRegistry.keystore + ); + } + + // KSQL + if (data.ksql) { + config.ksqldbServer = data.ksql.url; + config.ksqldbServerAuth = transformToCredentials( + data.ksql.isAuth, + data.ksql.username, + data.ksql.password + ); + config.ksqldbServerSsl = transformToKeystore(data.ksql.keystore); + } + + // Kafka Connect + if (data.kafkaConnect && data.kafkaConnect.length > 0) { + config.kafkaConnect = data.kafkaConnect.map( + ({ name, address, isAuth, username, password, keystore }) => ({ + name, + address, + ...transformToKeystore(keystore), + ...transformToCredentials(isAuth, username, password), + }) + ); + } + + // Metrics + if (data.metrics) { + config.metrics = { + type: data.metrics.type, + port: Number(data.metrics.port), + ...transformToKeystore(data.metrics.keystore), + ...transformToCredentials( + data.metrics.isAuth, + data.metrics.username, + data.metrics.password + ), + }; + } + + config.properties = { + ...transformCustomProps(data.customAuth), + }; + + // Authentication + if (data.auth) { + const { method, props, securityProtocol, keystore } = data.auth; + switch (method) { + case 'SASL/JAAS': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.jaas.config': props.saslJaasConfig, + 'sasl.mechanism': props.saslMechanism, + }; + break; + case 'SASL/GSSAPI': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.mechanism': 'GSSAPI', + 'sasl.kerberos.service.name': props.saslKerberosServiceName, + 'sasl.jaas.config': getJaasConfig('SASL/GSSAPI', { + useKeytab: props.keyTabFile ? 'true' : 'false', + keyTab: props.keyTabFile, + storeKey: String(!!props.storeKey), + principal: props.principal, + }), + }; + break; + case 'SASL/OAUTHBEARER': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.mechanism': 'OAUTHBEARER', + 'sasl.jaas.config': getJaasConfig('SASL/OAUTHBEARER', { + unsecuredLoginStringClaim_sub: props.unsecuredLoginStringClaim_sub, + }), + }; + break; + case 'SASL/PLAIN': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.mechanism': 'PLAIN', + 'sasl.jaas.config': getJaasConfig( + 'SASL/PLAIN', + transformToCredentials( + Boolean(props.isAuth), + props.username, + props.password + ) || {} + ), + }; + break; + case 'SASL/SCRAM-256': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.mechanism': 'SCRAM-SHA-256', + 'sasl.jaas.config': getJaasConfig( + 'SASL/SCRAM-256', + transformToCredentials( + Boolean(props.isAuth), + props.username, + props.password + ) || {} + ), + }; + break; + case 'SASL/SCRAM-512': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.mechanism': 'SCRAM-SHA-512', + 'sasl.jaas.config': getJaasConfig( + 'SASL/SCRAM-512', + transformToCredentials( + Boolean(props.isAuth), + props.username, + props.password + ) || {} + ), + }; + break; + case 'Delegation tokens': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.jaas.config': getJaasConfig('Delegation tokens', { + username: props.tokenId, + password: props.tokenValue, + tokenauth: 'true', + }), + }; + break; + case 'SASL/LDAP': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.mechanism': 'PLAIN', + 'sasl.jaas.config': getJaasConfig( + 'SASL/LDAP', + transformToCredentials( + Boolean(props.isAuth), + props.username, + props.password + ) || {} + ), + }; + break; + case 'SASL/AWS IAM': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.mechanism': 'AWS_MSK_IAM', + 'sasl.client.callback.handler.class': + 'software.amazon.msk.auth.iam.IAMClientCallbackHandler', + 'sasl.jaas.config': getJaasConfig('SASL/AWS IAM', { + awsProfileName: props.awsProfileName, + }), + }; + break; + case 'mTLS': + config.properties = { + 'security.protocol': 'SSL', + 'ssl.keystore.location': keystore?.location, + 'ssl.keystore.password': keystore?.password, + }; + break; + default: + // do nothing + } + } + + return config; +}; diff --git a/pom.xml b/pom.xml index 9abb41a865..dc1f50f1a2 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ latest - 4.7.1 + 4.12.0 2.11.1 3.19.0 1.11.1 @@ -42,10 +42,12 @@ 2.7.5 5.7.5 1.0.0 + 0.1.15 + 0.1.19 5.9.1 - 4.9.0 + 5.1.1 4.10.0 1.17.5