Merge branch 'master' of github.com:provectus/kafka-ui into ISSUE_754_acl

 Conflicts:
	kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java
This commit is contained in:
iliax 2023-04-11 12:09:47 +04:00
commit a58c2055f5
182 changed files with 6467 additions and 5149 deletions

View file

@ -0,0 +1,36 @@
{
"name": "Java",
"image": "mcr.microsoft.com/devcontainers/java:0-17",
"features": {
"ghcr.io/devcontainers/features/java:1": {
"version": "none",
"installMaven": "true",
"installGradle": "false"
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "java -version",
"customizations": {
"vscode": {
"extensions" : [
"vscjava.vscode-java-pack",
"vscjava.vscode-maven",
"vscjava.vscode-java-debug",
"EditorConfig.EditorConfig",
"ms-azuretools.vscode-docker",
"antfu.vite",
"ms-kubernetes-tools.vscode-kubernetes-tools",
"github.vscode-pull-request-github"
]
}
}
}

View file

@ -36,7 +36,7 @@ jobs:
- name: Pull with Docker - name: Pull with Docker
id: pull_chrome id: pull_chrome
run: | run: |
docker pull selenium/standalone-chrome:103.0 docker pull selenoid/vnc_chrome:103.0
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
@ -52,6 +52,7 @@ jobs:
id: compose_app id: compose_app
# use the following command until #819 will be fixed # use the following command until #819 will be fixed
run: | run: |
docker-compose -f kafka-ui-e2e-checks/docker/selenoid-git.yaml up -d
docker-compose -f ./documentation/compose/e2e-tests.yaml up -d docker-compose -f ./documentation/compose/e2e-tests.yaml up -d
- name: Run test suite - name: Run test suite
run: | run: |
@ -78,7 +79,7 @@ jobs:
uses: Sibz/github-status-action@v1.1.6 uses: Sibz/github-status-action@v1.1.6
with: with:
authToken: ${{secrets.GITHUB_TOKEN}} authToken: ${{secrets.GITHUB_TOKEN}}
context: "Test report" context: "Click Details button to open Allure report"
state: "success" state: "success"
sha: ${{ github.sha }} sha: ${{ github.sha }}
target_url: http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com/${{ github.run_number }} target_url: http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com/${{ github.run_number }}

View file

@ -15,20 +15,20 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Configure AWS credentials for Kafka-UI account - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2 uses: aws-actions/configure-aws-credentials@v2
with: with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-central-1 aws-region: eu-central-1
- name: Set the values - name: Set up environment
id: set_env_values id: set_env_values
run: | run: |
cat "./kafka-ui-e2e-checks/.env.ci" >> "./kafka-ui-e2e-checks/.env" cat "./kafka-ui-e2e-checks/.env.ci" >> "./kafka-ui-e2e-checks/.env"
- name: pull docker - name: Pull with Docker
id: pull_chrome id: pull_chrome
run: | run: |
docker pull selenium/standalone-chrome:103.0 docker pull selenoid/vnc_chrome:103.0
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
@ -40,12 +40,13 @@ jobs:
run: | run: |
./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.pull_request.head.sha }} ./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.pull_request.head.sha }}
./mvnw -B -V -ntp clean install -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 - name: Compose with Docker
id: compose_app id: compose_app
# use the following command until #819 will be fixed # use the following command until #819 will be fixed
run: | run: |
docker-compose -f kafka-ui-e2e-checks/docker/selenoid-git.yaml up -d
docker-compose -f ./documentation/compose/e2e-tests.yaml up -d docker-compose -f ./documentation/compose/e2e-tests.yaml up -d
- name: e2e run - name: Run test suite
run: | run: |
./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.pull_request.head.sha }} ./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.pull_request.head.sha }}
./mvnw -B -V -ntp -Dsurefire.suiteXmlFiles='src/test/resources/smoke.xml' -f 'kafka-ui-e2e-checks' test -Pprod ./mvnw -B -V -ntp -Dsurefire.suiteXmlFiles='src/test/resources/smoke.xml' -f 'kafka-ui-e2e-checks' test -Pprod
@ -65,7 +66,7 @@ jobs:
AWS_S3_BUCKET: 'kafkaui-allure-reports' AWS_S3_BUCKET: 'kafkaui-allure-reports'
AWS_REGION: 'eu-central-1' AWS_REGION: 'eu-central-1'
SOURCE_DIR: 'allure-history/allure-results' SOURCE_DIR: 'allure-history/allure-results'
- name: Post the link to allure report - name: Deploy report to Amazon S3
if: always() if: always()
uses: Sibz/github-status-action@v1.1.6 uses: Sibz/github-status-action@v1.1.6
with: with:

View file

@ -23,7 +23,7 @@ jobs:
- name: Pull with Docker - name: Pull with Docker
id: pull_chrome id: pull_chrome
run: | run: |
docker pull selenium/standalone-chrome:103.0 docker pull selenoid/vnc_chrome:103.0
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
@ -39,6 +39,7 @@ jobs:
id: compose_app id: compose_app
# use the following command until #819 will be fixed # use the following command until #819 will be fixed
run: | run: |
docker-compose -f kafka-ui-e2e-checks/docker/selenoid-git.yaml up -d
docker-compose -f ./documentation/compose/e2e-tests.yaml up -d docker-compose -f ./documentation/compose/e2e-tests.yaml up -d
- name: Run test suite - name: Run test suite
run: | run: |
@ -65,7 +66,7 @@ jobs:
uses: Sibz/github-status-action@v1.1.6 uses: Sibz/github-status-action@v1.1.6
with: with:
authToken: ${{secrets.GITHUB_TOKEN}} authToken: ${{secrets.GITHUB_TOKEN}}
context: "Test report" context: "Click Details button to open Allure report"
state: "success" state: "success"
sha: ${{ github.sha }} sha: ${{ github.sha }}
target_url: http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com/${{ github.run_number }} target_url: http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com/${{ github.run_number }}

View file

@ -11,14 +11,14 @@ services:
test: wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health test: wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 10 retries: 10
depends_on: depends_on:
kafka0: kafka0:
condition: service_healthy condition: service_healthy
schemaregistry0: schemaregistry0:
condition: service_healthy condition: service_healthy
kafka-connect0: kafka-connect0:
condition: service_healthy condition: service_healthy
environment: environment:
KAFKA_CLUSTERS_0_NAME: local KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092
@ -33,10 +33,10 @@ services:
hostname: kafka0 hostname: kafka0
container_name: kafka0 container_name: kafka0
healthcheck: healthcheck:
test: unset JMX_PORT && KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9999" && kafka-broker-api-versions --bootstrap-server=localhost:9092 test: unset JMX_PORT && KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9999" && kafka-broker-api-versions --bootstrap-server=localhost:9092
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 10 retries: 10
ports: ports:
- "9092:9092" - "9092:9092"
- "9997:9997" - "9997:9997"
@ -68,12 +68,12 @@ services:
- 8085:8085 - 8085:8085
depends_on: depends_on:
kafka0: kafka0:
condition: service_healthy condition: service_healthy
healthcheck: healthcheck:
test: ["CMD", "timeout", "1", "curl", "--silent", "--fail", "http://schemaregistry0:8085/subjects"] test: [ "CMD", "timeout", "1", "curl", "--silent", "--fail", "http://schemaregistry0:8085/subjects" ]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 10 retries: 10
environment: environment:
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092 SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092
SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT
@ -93,11 +93,11 @@ services:
- 8083:8083 - 8083:8083
depends_on: depends_on:
kafka0: kafka0:
condition: service_healthy condition: service_healthy
schemaregistry0: schemaregistry0:
condition: service_healthy condition: service_healthy
healthcheck: healthcheck:
test: ["CMD", "nc", "127.0.0.1", "8083"] test: [ "CMD", "nc", "127.0.0.1", "8083" ]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 10 retries: 10
@ -118,8 +118,8 @@ services:
CONNECT_INTERNAL_VALUE_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_REST_ADVERTISED_HOST_NAME: kafka-connect0
CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components" CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components"
# AWS_ACCESS_KEY_ID: "" # AWS_ACCESS_KEY_ID: ""
# AWS_SECRET_ACCESS_KEY: "" # AWS_SECRET_ACCESS_KEY: ""
kafka-init-topics: kafka-init-topics:
image: confluentinc/cp-kafka:7.2.1 image: confluentinc/cp-kafka:7.2.1
@ -127,7 +127,7 @@ services:
- ./message.json:/data/message.json - ./message.json:/data/message.json
depends_on: depends_on:
kafka0: kafka0:
condition: service_healthy condition: service_healthy
command: "bash -c 'echo Waiting for Kafka to be ready... && \ command: "bash -c 'echo Waiting for Kafka to be ready... && \
cub kafka-ready -b kafka0:29092 1 30 && \ cub kafka-ready -b kafka0:29092 1 30 && \
kafka-topics --create --topic users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \ kafka-topics --create --topic users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \
@ -142,10 +142,10 @@ services:
ports: ports:
- 5432:5432 - 5432:5432
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U dev_user"] test: [ "CMD-SHELL", "pg_isready -U dev_user" ]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
environment: environment:
POSTGRES_USER: 'dev_user' POSTGRES_USER: 'dev_user'
POSTGRES_PASSWORD: '12345' POSTGRES_PASSWORD: '12345'
@ -154,7 +154,7 @@ services:
image: ellerbrock/alpine-bash-curl-ssl image: ellerbrock/alpine-bash-curl-ssl
depends_on: depends_on:
postgres-db: postgres-db:
condition: service_healthy condition: service_healthy
kafka-connect0: kafka-connect0:
condition: service_healthy condition: service_healthy
volumes: volumes:
@ -164,7 +164,7 @@ services:
ksqldb: ksqldb:
image: confluentinc/ksqldb-server:0.18.0 image: confluentinc/ksqldb-server:0.18.0
healthcheck: healthcheck:
test: ["CMD", "timeout", "1", "curl", "--silent", "--fail", "http://localhost:8088/info"] test: [ "CMD", "timeout", "1", "curl", "--silent", "--fail", "http://localhost:8088/info" ]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 10 retries: 10
@ -174,7 +174,7 @@ services:
kafka-connect0: kafka-connect0:
condition: service_healthy condition: service_healthy
schemaregistry0: schemaregistry0:
condition: service_healthy condition: service_healthy
ports: ports:
- 8088:8088 - 8088:8088
environment: environment:

View file

@ -0,0 +1,333 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<!--
Checkstyle configuration that checks the Google coding conventions from Google Java Style
that can be found at https://google.github.io/styleguide/javaguide.html
Checkstyle is very configurable. Be sure to read the documentation at
http://checkstyle.org (or in your downloaded distribution).
To completely disable a check, just comment it out or delete it from the file.
To suppress certain violations please review suppression filters.
Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.
-->
<module name = "Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="warning"/>
<property name="fileExtensions" value="java, properties, xml"/>
<!-- Excludes all 'module-info.java' files -->
<!-- See https://checkstyle.org/config_filefilters.html -->
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="module\-info\.java$"/>
</module>
<!-- https://checkstyle.org/config_filters.html#SuppressionFilter -->
<module name="SuppressionFilter">
<property name="file" value="${org.checkstyle.google.suppressionfilter.config}"
default="checkstyle-suppressions.xml" />
<property name="optional" value="true"/>
</module>
<!-- Checks for whitespace -->
<!-- See http://checkstyle.org/config_whitespace.html -->
<module name="FileTabCharacter">
<property name="eachLine" value="true"/>
</module>
<module name="LineLength">
<property name="fileExtensions" value="java"/>
<property name="max" value="120"/>
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
</module>
<module name="TreeWalker">
<module name="OuterTypeFilename"/>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
<property name="format"
value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
<property name="message"
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
</module>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<module name="AvoidStarImport"/>
<module name="OneTopLevelClass"/>
<module name="NoLineWrap">
<property name="tokens" value="PACKAGE_DEF, IMPORT, STATIC_IMPORT"/>
</module>
<module name="EmptyBlock">
<property name="option" value="TEXT"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
</module>
<module name="NeedBraces">
<property name="tokens"
value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE"/>
</module>
<module name="LeftCurly">
<property name="tokens"
value="ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,
INTERFACE_DEF, LAMBDA, LITERAL_CASE, LITERAL_CATCH, LITERAL_DEFAULT,
LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF,
OBJBLOCK, STATIC_INIT"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlySame"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,
LITERAL_DO"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlyAlone"/>
<property name="option" value="alone"/>
<property name="tokens"
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF"/>
</module>
<module name="SuppressionXpathSingleFilter">
<!-- suppresion is required till https://github.com/checkstyle/checkstyle/issues/7541 -->
<property name="id" value="RightCurlyAlone"/>
<property name="query" value="//RCURLY[parent::SLIST[count(./*)=1]
or preceding-sibling::*[last()][self::LCURLY]]"/>
</module>
<module name="WhitespaceAfter">
<property name="tokens"
value="COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE,
LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, DO_WHILE"/>
</module>
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true"/>
<property name="allowEmptyLambdas" value="true"/>
<property name="allowEmptyMethods" value="true"/>
<property name="allowEmptyTypes" value="true"/>
<property name="allowEmptyLoops" value="true"/>
<property name="tokens"
value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,
BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND,
LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,
LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED,
LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,
NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,
SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND"/>
<message key="ws.notFollowed"
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
<message key="ws.notPreceded"
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
</module>
<module name="OneStatementPerLine"/>
<!-- <module name="MultipleVariableDeclarations"/>-->
<module name="ArrayTypeStyle"/>
<module name="MissingSwitchDefault"/>
<module name="FallThrough"/>
<module name="UpperEll"/>
<module name="ModifierOrder"/>
<module name="EmptyLineSeparator">
<property name="tokens"
value="PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
<property name="allowNoEmptyLineBetweenFields" value="true"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapDot"/>
<property name="tokens" value="DOT"/>
<property name="option" value="nl"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapComma"/>
<property name="tokens" value="COMMA"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 -->
<property name="id" value="SeparatorWrapEllipsis"/>
<property name="tokens" value="ELLIPSIS"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 -->
<property name="id" value="SeparatorWrapArrayDeclarator"/>
<property name="tokens" value="ARRAY_DECLARATOR"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapMethodRef"/>
<property name="tokens" value="METHOD_REF"/>
<property name="option" value="nl"/>
</module>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern"
value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="TypeName">
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF"/>
<message key="name.invalidPattern"
value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MemberName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern"
value="Member name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LambdaParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="CatchParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LocalVariableName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="InterfaceTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Interface type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="NoFinalizer"/>
<module name="GenericWhitespace">
<message key="ws.followed"
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
<message key="ws.preceded"
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
<message key="ws.illegalFollow"
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
<message key="ws.notPreceded"
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
</module>
<module name="Indentation">
<property name="basicOffset" value="2"/>
<property name="braceAdjustment" value="0"/>
<property name="caseIndent" value="2"/>
<property name="throwsIndent" value="4"/>
<property name="lineWrappingIndentation" value="4"/>
<property name="arrayInitIndent" value="2"/>
</module>
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="1"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,
PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF"/>
</module>
<module name="OverloadMethodsDeclarationOrder"/>
<!-- <module name="VariableDeclarationUsageDistance"/>-->
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>
<property name="separateLineBetweenGroups" value="true"/>
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
<property name="tokens" value="IMPORT, STATIC_IMPORT, PACKAGE_DEF"/>
</module>
<module name="MethodParamPad">
<property name="tokens"
value="CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF,
SUPER_CTOR_CALL, ENUM_CONSTANT_DEF"/>
</module>
<module name="NoWhitespaceBefore">
<property name="tokens"
value="COMMA, SEMI, POST_INC, POST_DEC, DOT, ELLIPSIS,
LABELED_STAT, METHOD_REF"/>
<property name="allowLineBreaks" value="true"/>
</module>
<module name="ParenPad">
<property name="tokens"
value="ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,
EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL,
METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA"/>
</module>
<module name="OperatorWrap">
<property name="option" value="NL"/>
<property name="tokens"
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF "/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationMostCases"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationVariables"/>
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<module name="NonEmptyAtclauseDescription"/>
<module name="InvalidJavadocPosition"/>
<module name="JavadocTagContinuationIndentation"/>
<module name="SummaryJavadoc">
<property name="forbiddenSummaryFragments"
value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
</module>
<module name="JavadocParagraph"/>
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
<property name="target"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="JavadocMethod">
<property name="accessModifiers" value="public"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF"/>
</module>
<!-- <module name="MissingJavadocMethod">-->
<!-- <property name="scope" value="public"/>-->
<!-- <property name="minLineCount" value="2"/>-->
<!-- <property name="allowedAnnotations" value="Override, Test"/>-->
<!-- <property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF"/>-->
<!-- </module>-->
<module name="MethodName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>
<message key="name.invalidPattern"
value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="SingleLineJavadoc">
<property name="ignoreInlineTags" value="false"/>
</module>
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="ignored"/>
</module>
<module name="CommentsIndentation">
<property name="tokens" value="SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN"/>
</module>
<!-- https://checkstyle.org/config_filters.html#SuppressionXpathFilter -->
<module name="SuppressionXpathFilter">
<property name="file" value="${org.checkstyle.google.suppressionxpathfilter.config}"
default="checkstyle-xpath-suppressions.xml" />
<property name="optional" value="true"/>
</module>
</module>
</module>

View file

@ -318,7 +318,7 @@
<property name="ignoreInlineTags" value="false"/> <property name="ignoreInlineTags" value="false"/>
</module> </module>
<module name="EmptyCatchBlock"> <module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected"/> <property name="exceptionVariableName" value="ignored"/>
</module> </module>
<module name="CommentsIndentation"> <module name="CommentsIndentation">
<property name="tokens" value="SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN"/> <property name="tokens" value="SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN"/>
@ -330,4 +330,4 @@
<property name="optional" value="true"/> <property name="optional" value="true"/>
</module> </module>
</module> </module>
</module> </module>

View file

@ -1,4 +1,5 @@
FROM azul/zulu-openjdk-alpine:17-jre #FROM azul/zulu-openjdk-alpine:17-jre-headless
FROM azul/zulu-openjdk-alpine@sha256:a36679ac0d28cb835e2a8c00e1e0d95509c6c51c5081c7782b85edb1f37a771a
RUN apk add --no-cache gcompat # need to make snappy codec work RUN apk add --no-cache gcompat # need to make snappy codec work
RUN addgroup -S kafkaui && adduser -S kafkaui -G kafkaui RUN addgroup -S kafkaui && adduser -S kafkaui -G kafkaui

View file

@ -6,7 +6,13 @@ import com.provectus.kafka.ui.config.ClustersProperties;
import com.provectus.kafka.ui.connect.ApiClient; import com.provectus.kafka.ui.connect.ApiClient;
import com.provectus.kafka.ui.connect.api.KafkaConnectClientApi; import com.provectus.kafka.ui.connect.api.KafkaConnectClientApi;
import com.provectus.kafka.ui.connect.model.Connector; import com.provectus.kafka.ui.connect.model.Connector;
import com.provectus.kafka.ui.connect.model.ConnectorPlugin;
import com.provectus.kafka.ui.connect.model.ConnectorPluginConfigValidationResponse;
import com.provectus.kafka.ui.connect.model.ConnectorStatus;
import com.provectus.kafka.ui.connect.model.ConnectorTask;
import com.provectus.kafka.ui.connect.model.ConnectorTopics;
import com.provectus.kafka.ui.connect.model.NewConnector; import com.provectus.kafka.ui.connect.model.NewConnector;
import com.provectus.kafka.ui.connect.model.TaskStatus;
import com.provectus.kafka.ui.exception.KafkaConnectConflictReponseException; import com.provectus.kafka.ui.exception.KafkaConnectConflictReponseException;
import com.provectus.kafka.ui.exception.ValidationException; import com.provectus.kafka.ui.exception.ValidationException;
import com.provectus.kafka.ui.util.WebClientConfigurator; import com.provectus.kafka.ui.util.WebClientConfigurator;
@ -15,11 +21,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.MultiValueMap;
import org.springframework.util.unit.DataSize; import org.springframework.util.unit.DataSize;
import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestClientException;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
@ -79,6 +81,176 @@ public class RetryingKafkaConnectClient extends KafkaConnectClientApi {
); );
} }
@Override
public Mono<ResponseEntity<Connector>> createConnectorWithHttpInfo(NewConnector newConnector)
throws WebClientResponseException {
return withRetryOnConflict(super.createConnectorWithHttpInfo(newConnector));
}
@Override
public Mono<Void> deleteConnector(String connectorName) throws WebClientResponseException {
return withRetryOnConflict(super.deleteConnector(connectorName));
}
@Override
public Mono<ResponseEntity<Void>> deleteConnectorWithHttpInfo(String connectorName)
throws WebClientResponseException {
return withRetryOnConflict(super.deleteConnectorWithHttpInfo(connectorName));
}
@Override
public Mono<Connector> getConnector(String connectorName) throws WebClientResponseException {
return withRetryOnConflict(super.getConnector(connectorName));
}
@Override
public Mono<ResponseEntity<Connector>> getConnectorWithHttpInfo(String connectorName)
throws WebClientResponseException {
return withRetryOnConflict(super.getConnectorWithHttpInfo(connectorName));
}
@Override
public Mono<Map<String, Object>> getConnectorConfig(String connectorName) throws WebClientResponseException {
return withRetryOnConflict(super.getConnectorConfig(connectorName));
}
@Override
public Mono<ResponseEntity<Map<String, Object>>> getConnectorConfigWithHttpInfo(String connectorName)
throws WebClientResponseException {
return withRetryOnConflict(super.getConnectorConfigWithHttpInfo(connectorName));
}
@Override
public Flux<ConnectorPlugin> getConnectorPlugins() throws WebClientResponseException {
return withRetryOnConflict(super.getConnectorPlugins());
}
@Override
public Mono<ResponseEntity<List<ConnectorPlugin>>> getConnectorPluginsWithHttpInfo()
throws WebClientResponseException {
return withRetryOnConflict(super.getConnectorPluginsWithHttpInfo());
}
@Override
public Mono<ConnectorStatus> getConnectorStatus(String connectorName) throws WebClientResponseException {
return withRetryOnConflict(super.getConnectorStatus(connectorName));
}
@Override
public Mono<ResponseEntity<ConnectorStatus>> getConnectorStatusWithHttpInfo(String connectorName)
throws WebClientResponseException {
return withRetryOnConflict(super.getConnectorStatusWithHttpInfo(connectorName));
}
@Override
public Mono<TaskStatus> getConnectorTaskStatus(String connectorName, Integer taskId)
throws WebClientResponseException {
return withRetryOnConflict(super.getConnectorTaskStatus(connectorName, taskId));
}
@Override
public Mono<ResponseEntity<TaskStatus>> getConnectorTaskStatusWithHttpInfo(String connectorName, Integer taskId)
throws WebClientResponseException {
return withRetryOnConflict(super.getConnectorTaskStatusWithHttpInfo(connectorName, taskId));
}
@Override
public Flux<ConnectorTask> getConnectorTasks(String connectorName) throws WebClientResponseException {
return withRetryOnConflict(super.getConnectorTasks(connectorName));
}
@Override
public Mono<ResponseEntity<List<ConnectorTask>>> getConnectorTasksWithHttpInfo(String connectorName)
throws WebClientResponseException {
return withRetryOnConflict(super.getConnectorTasksWithHttpInfo(connectorName));
}
@Override
public Mono<Map<String, ConnectorTopics>> getConnectorTopics(String connectorName) throws WebClientResponseException {
return withRetryOnConflict(super.getConnectorTopics(connectorName));
}
@Override
public Mono<ResponseEntity<Map<String, ConnectorTopics>>> getConnectorTopicsWithHttpInfo(String connectorName)
throws WebClientResponseException {
return withRetryOnConflict(super.getConnectorTopicsWithHttpInfo(connectorName));
}
@Override
public Flux<String> getConnectors(String search) throws WebClientResponseException {
return withRetryOnConflict(super.getConnectors(search));
}
@Override
public Mono<ResponseEntity<List<String>>> getConnectorsWithHttpInfo(String search) throws WebClientResponseException {
return withRetryOnConflict(super.getConnectorsWithHttpInfo(search));
}
@Override
public Mono<Void> pauseConnector(String connectorName) throws WebClientResponseException {
return withRetryOnConflict(super.pauseConnector(connectorName));
}
@Override
public Mono<ResponseEntity<Void>> pauseConnectorWithHttpInfo(String connectorName) throws WebClientResponseException {
return withRetryOnConflict(super.pauseConnectorWithHttpInfo(connectorName));
}
@Override
public Mono<Void> restartConnector(String connectorName, Boolean includeTasks, Boolean onlyFailed)
throws WebClientResponseException {
return withRetryOnConflict(super.restartConnector(connectorName, includeTasks, onlyFailed));
}
@Override
public Mono<ResponseEntity<Void>> restartConnectorWithHttpInfo(String connectorName, Boolean includeTasks,
Boolean onlyFailed) throws WebClientResponseException {
return withRetryOnConflict(super.restartConnectorWithHttpInfo(connectorName, includeTasks, onlyFailed));
}
@Override
public Mono<Void> restartConnectorTask(String connectorName, Integer taskId) throws WebClientResponseException {
return withRetryOnConflict(super.restartConnectorTask(connectorName, taskId));
}
@Override
public Mono<ResponseEntity<Void>> restartConnectorTaskWithHttpInfo(String connectorName, Integer taskId)
throws WebClientResponseException {
return withRetryOnConflict(super.restartConnectorTaskWithHttpInfo(connectorName, taskId));
}
@Override
public Mono<Void> resumeConnector(String connectorName) throws WebClientResponseException {
return super.resumeConnector(connectorName);
}
@Override
public Mono<ResponseEntity<Void>> resumeConnectorWithHttpInfo(String connectorName)
throws WebClientResponseException {
return withRetryOnConflict(super.resumeConnectorWithHttpInfo(connectorName));
}
@Override
public Mono<ResponseEntity<Connector>> setConnectorConfigWithHttpInfo(String connectorName,
Map<String, Object> requestBody)
throws WebClientResponseException {
return withRetryOnConflict(super.setConnectorConfigWithHttpInfo(connectorName, requestBody));
}
@Override
public Mono<ConnectorPluginConfigValidationResponse> validateConnectorPluginConfig(String pluginName,
Map<String, Object> requestBody)
throws WebClientResponseException {
return withRetryOnConflict(super.validateConnectorPluginConfig(pluginName, requestBody));
}
@Override
public Mono<ResponseEntity<ConnectorPluginConfigValidationResponse>> validateConnectorPluginConfigWithHttpInfo(
String pluginName, Map<String, Object> requestBody) throws WebClientResponseException {
return withRetryOnConflict(super.validateConnectorPluginConfigWithHttpInfo(pluginName, requestBody));
}
private static class RetryingApiClient extends ApiClient { private static class RetryingApiClient extends ApiClient {
public RetryingApiClient(ConnectCluster config, public RetryingApiClient(ConnectCluster config,
@ -108,35 +280,5 @@ public class RetryingKafkaConnectClient extends KafkaConnectClientApi {
.configureBufferSize(maxBuffSize) .configureBufferSize(maxBuffSize)
.build(); .build();
} }
@Override
public <T> Mono<T> invokeAPI(String path, HttpMethod method, Map<String, Object> pathParams,
MultiValueMap<String, String> queryParams, Object body,
HttpHeaders headerParams,
MultiValueMap<String, String> cookieParams,
MultiValueMap<String, Object> formParams, List<MediaType> accept,
MediaType contentType, String[] authNames,
ParameterizedTypeReference<T> returnType)
throws RestClientException {
return withRetryOnConflict(
super.invokeAPI(path, method, pathParams, queryParams, body, headerParams, cookieParams,
formParams, accept, contentType, authNames, returnType)
);
}
@Override
public <T> Flux<T> invokeFluxAPI(String path, HttpMethod method, Map<String, Object> pathParams,
MultiValueMap<String, String> queryParams, Object body,
HttpHeaders headerParams,
MultiValueMap<String, String> cookieParams,
MultiValueMap<String, Object> formParams,
List<MediaType> accept, MediaType contentType,
String[] authNames, ParameterizedTypeReference<T> returnType)
throws RestClientException {
return withRetryOnConflict(
super.invokeFluxAPI(path, method, pathParams, queryParams, body, headerParams,
cookieParams, formParams, accept, contentType, authNames, returnType)
);
}
} }
} }

View file

@ -1,6 +1,7 @@
package com.provectus.kafka.ui.config; package com.provectus.kafka.ui.config;
import com.provectus.kafka.ui.model.MetricsConfig; import com.provectus.kafka.ui.model.MetricsConfig;
import jakarta.annotation.PostConstruct;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@ -8,7 +9,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;

View file

@ -1,9 +1,9 @@
package com.provectus.kafka.ui.config.auth; package com.provectus.kafka.ui.config.auth;
import jakarta.annotation.PostConstruct;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.annotation.PostConstruct;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.Assert; import org.springframework.util.Assert;

View file

@ -13,12 +13,12 @@ import com.provectus.kafka.ui.model.ClusterConfigValidationDTO;
import com.provectus.kafka.ui.model.RestartRequestDTO; import com.provectus.kafka.ui.model.RestartRequestDTO;
import com.provectus.kafka.ui.model.UploadedFileInfoDTO; import com.provectus.kafka.ui.model.UploadedFileInfoDTO;
import com.provectus.kafka.ui.model.rbac.AccessContext; import com.provectus.kafka.ui.model.rbac.AccessContext;
import com.provectus.kafka.ui.service.ApplicationInfoService;
import com.provectus.kafka.ui.service.KafkaClusterFactory; import com.provectus.kafka.ui.service.KafkaClusterFactory;
import com.provectus.kafka.ui.service.rbac.AccessControlService; import com.provectus.kafka.ui.service.rbac.AccessControlService;
import com.provectus.kafka.ui.util.ApplicationRestarter; import com.provectus.kafka.ui.util.ApplicationRestarter;
import com.provectus.kafka.ui.util.DynamicConfigOperations; import com.provectus.kafka.ui.util.DynamicConfigOperations;
import com.provectus.kafka.ui.util.DynamicConfigOperations.PropertiesStructure; import com.provectus.kafka.ui.util.DynamicConfigOperations.PropertiesStructure;
import java.util.List;
import java.util.Map; import java.util.Map;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -53,18 +53,11 @@ public class ApplicationConfigController implements ApplicationConfigApi {
private final DynamicConfigOperations dynamicConfigOperations; private final DynamicConfigOperations dynamicConfigOperations;
private final ApplicationRestarter restarter; private final ApplicationRestarter restarter;
private final KafkaClusterFactory kafkaClusterFactory; private final KafkaClusterFactory kafkaClusterFactory;
private final ApplicationInfoService applicationInfoService;
@Override @Override
public Mono<ResponseEntity<ApplicationInfoDTO>> getApplicationInfo(ServerWebExchange exchange) { public Mono<ResponseEntity<ApplicationInfoDTO>> getApplicationInfo(ServerWebExchange exchange) {
return Mono.just( return Mono.just(applicationInfoService.getApplicationInfo()).map(ResponseEntity::ok);
new ApplicationInfoDTO()
.enabledFeatures(
dynamicConfigOperations.dynamicConfigEnabled()
? List.of(ApplicationInfoDTO.EnabledFeaturesEnum.DYNAMIC_CONFIG)
: List.of()
)
).map(ResponseEntity::ok);
} }
@Override @Override

View file

@ -149,10 +149,9 @@ public class KafkaConnectController extends AbstractController implements KafkaC
} }
@Override @Override
public Mono<ResponseEntity<ConnectorDTO>> setConnectorConfig(String clusterName, public Mono<ResponseEntity<ConnectorDTO>> setConnectorConfig(String clusterName, String connectName,
String connectName,
String connectorName, String connectorName,
@Valid Mono<Object> requestBody, Mono<Map<String, Object>> requestBody,
ServerWebExchange exchange) { ServerWebExchange exchange) {
Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder() Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
@ -164,8 +163,7 @@ public class KafkaConnectController extends AbstractController implements KafkaC
return validateAccess.then( return validateAccess.then(
kafkaConnectService kafkaConnectService
.setConnectorConfig(getCluster(clusterName), connectName, connectorName, requestBody) .setConnectorConfig(getCluster(clusterName), connectName, connectorName, requestBody)
.map(ResponseEntity::ok) .map(ResponseEntity::ok));
);
} }
@Override @Override
@ -242,7 +240,7 @@ public class KafkaConnectController extends AbstractController implements KafkaC
@Override @Override
public Mono<ResponseEntity<ConnectorPluginConfigValidationResponseDTO>> validateConnectorPluginConfig( public Mono<ResponseEntity<ConnectorPluginConfigValidationResponseDTO>> validateConnectorPluginConfig(
String clusterName, String connectName, String pluginName, @Valid Mono<Object> requestBody, String clusterName, String connectName, String pluginName, @Valid Mono<Map<String, Object>> requestBody,
ServerWebExchange exchange) { ServerWebExchange exchange) {
return kafkaConnectService return kafkaConnectService
.validateConnectorPluginConfig( .validateConnectorPluginConfig(

View file

@ -1,9 +1,6 @@
package com.provectus.kafka.ui.emitter; package com.provectus.kafka.ui.emitter;
import com.provectus.kafka.ui.model.TopicMessageDTO;
import com.provectus.kafka.ui.model.TopicMessageEventDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO;
import com.provectus.kafka.ui.model.TopicMessagePhaseDTO;
import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import org.apache.kafka.clients.consumer.Consumer; import org.apache.kafka.clients.consumer.Consumer;
@ -14,13 +11,12 @@ import reactor.core.publisher.FluxSink;
public abstract class AbstractEmitter { public abstract class AbstractEmitter {
private final ConsumerRecordDeserializer recordDeserializer; private final MessagesProcessing messagesProcessing;
private final ConsumingStats consumingStats = new ConsumingStats();
private final PollingThrottler throttler; private final PollingThrottler throttler;
protected final PollingSettings pollingSettings; protected final PollingSettings pollingSettings;
protected AbstractEmitter(ConsumerRecordDeserializer recordDeserializer, PollingSettings pollingSettings) { protected AbstractEmitter(MessagesProcessing messagesProcessing, PollingSettings pollingSettings) {
this.recordDeserializer = recordDeserializer; this.messagesProcessing = messagesProcessing;
this.pollingSettings = pollingSettings; this.pollingSettings = pollingSettings;
this.throttler = pollingSettings.getPollingThrottler(); this.throttler = pollingSettings.getPollingThrottler();
} }
@ -40,39 +36,27 @@ public abstract class AbstractEmitter {
return records; return records;
} }
protected boolean sendLimitReached() {
return messagesProcessing.limitReached();
}
protected void sendMessage(FluxSink<TopicMessageEventDTO> sink, protected void sendMessage(FluxSink<TopicMessageEventDTO> sink,
ConsumerRecord<Bytes, Bytes> msg) { ConsumerRecord<Bytes, Bytes> msg) {
final TopicMessageDTO topicMessage = recordDeserializer.deserialize(msg); messagesProcessing.sendMsg(sink, msg);
sink.next(
new TopicMessageEventDTO()
.type(TopicMessageEventDTO.TypeEnum.MESSAGE)
.message(topicMessage)
);
} }
protected void sendPhase(FluxSink<TopicMessageEventDTO> sink, String name) { protected void sendPhase(FluxSink<TopicMessageEventDTO> sink, String name) {
sink.next( messagesProcessing.sendPhase(sink, name);
new TopicMessageEventDTO()
.type(TopicMessageEventDTO.TypeEnum.PHASE)
.phase(new TopicMessagePhaseDTO().name(name))
);
} }
protected int sendConsuming(FluxSink<TopicMessageEventDTO> sink, protected int sendConsuming(FluxSink<TopicMessageEventDTO> sink,
ConsumerRecords<Bytes, Bytes> records, ConsumerRecords<Bytes, Bytes> records,
long elapsed) { long elapsed) {
return consumingStats.sendConsumingEvt(sink, records, elapsed, getFilterApplyErrors(sink)); return messagesProcessing.sentConsumingInfo(sink, records, elapsed);
} }
protected void sendFinishStatsAndCompleteSink(FluxSink<TopicMessageEventDTO> sink) { protected void sendFinishStatsAndCompleteSink(FluxSink<TopicMessageEventDTO> sink) {
consumingStats.sendFinishEvent(sink, getFilterApplyErrors(sink)); messagesProcessing.sendFinishEvent(sink);
sink.complete(); sink.complete();
} }
protected Number getFilterApplyErrors(FluxSink<?> sink) {
return sink.contextView()
.<MessageFilterStats>getOrEmpty(MessageFilterStats.class)
.<Number>map(MessageFilterStats::getFilterApplyErrors)
.orElse(0);
}
} }

View file

@ -2,7 +2,6 @@ package com.provectus.kafka.ui.emitter;
import com.provectus.kafka.ui.model.ConsumerPosition; import com.provectus.kafka.ui.model.ConsumerPosition;
import com.provectus.kafka.ui.model.TopicMessageEventDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO;
import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@ -31,9 +30,9 @@ public class BackwardRecordEmitter
Supplier<KafkaConsumer<Bytes, Bytes>> consumerSupplier, Supplier<KafkaConsumer<Bytes, Bytes>> consumerSupplier,
ConsumerPosition consumerPosition, ConsumerPosition consumerPosition,
int messagesPerPage, int messagesPerPage,
ConsumerRecordDeserializer recordDeserializer, MessagesProcessing messagesProcessing,
PollingSettings pollingSettings) { PollingSettings pollingSettings) {
super(recordDeserializer, pollingSettings); super(messagesProcessing, pollingSettings);
this.consumerPosition = consumerPosition; this.consumerPosition = consumerPosition;
this.messagesPerPage = messagesPerPage; this.messagesPerPage = messagesPerPage;
this.consumerSupplier = consumerSupplier; this.consumerSupplier = consumerSupplier;
@ -52,7 +51,7 @@ public class BackwardRecordEmitter
int msgsToPollPerPartition = (int) Math.ceil((double) messagesPerPage / readUntilOffsets.size()); int msgsToPollPerPartition = (int) Math.ceil((double) messagesPerPage / readUntilOffsets.size());
log.debug("'Until' offsets for polling: {}", readUntilOffsets); log.debug("'Until' offsets for polling: {}", readUntilOffsets);
while (!sink.isCancelled() && !readUntilOffsets.isEmpty()) { while (!sink.isCancelled() && !readUntilOffsets.isEmpty() && !sendLimitReached()) {
new TreeMap<>(readUntilOffsets).forEach((tp, readToOffset) -> { new TreeMap<>(readUntilOffsets).forEach((tp, readToOffset) -> {
if (sink.isCancelled()) { if (sink.isCancelled()) {
return; //fast return in case of sink cancellation return; //fast return in case of sink cancellation
@ -61,8 +60,6 @@ public class BackwardRecordEmitter
long readFromOffset = Math.max(beginOffset, readToOffset - msgsToPollPerPartition); long readFromOffset = Math.max(beginOffset, readToOffset - msgsToPollPerPartition);
partitionPollIteration(tp, readFromOffset, readToOffset, consumer, sink) partitionPollIteration(tp, readFromOffset, readToOffset, consumer, sink)
.stream()
.filter(r -> !sink.isCancelled())
.forEach(r -> sendMessage(sink, r)); .forEach(r -> sendMessage(sink, r));
if (beginOffset == readFromOffset) { if (beginOffset == readFromOffset) {
@ -106,6 +103,7 @@ public class BackwardRecordEmitter
EmptyPollsCounter emptyPolls = pollingSettings.createEmptyPollsCounter(); EmptyPollsCounter emptyPolls = pollingSettings.createEmptyPollsCounter();
while (!sink.isCancelled() while (!sink.isCancelled()
&& !sendLimitReached()
&& recordsToSend.size() < desiredMsgsToPoll && recordsToSend.size() < desiredMsgsToPoll
&& !emptyPolls.noDataEmptyPollsReached()) { && !emptyPolls.noDataEmptyPollsReached()) {
var polledRecords = poll(sink, consumer, pollingSettings.getPartitionPollTimeout()); var polledRecords = poll(sink, consumer, pollingSettings.getPartitionPollTimeout());

View file

@ -19,7 +19,7 @@ class ConsumingStats {
int sendConsumingEvt(FluxSink<TopicMessageEventDTO> sink, int sendConsumingEvt(FluxSink<TopicMessageEventDTO> sink,
ConsumerRecords<Bytes, Bytes> polledRecords, ConsumerRecords<Bytes, Bytes> polledRecords,
long elapsed, long elapsed,
Number filterApplyErrors) { int filterApplyErrors) {
int polledBytes = ConsumerRecordsUtil.calculatePolledSize(polledRecords); int polledBytes = ConsumerRecordsUtil.calculatePolledSize(polledRecords);
bytes += polledBytes; bytes += polledBytes;
this.records += polledRecords.count(); this.records += polledRecords.count();
@ -32,7 +32,7 @@ class ConsumingStats {
return polledBytes; return polledBytes;
} }
void sendFinishEvent(FluxSink<TopicMessageEventDTO> sink, Number filterApplyErrors) { void sendFinishEvent(FluxSink<TopicMessageEventDTO> sink, int filterApplyErrors) {
sink.next( sink.next(
new TopicMessageEventDTO() new TopicMessageEventDTO()
.type(TopicMessageEventDTO.TypeEnum.DONE) .type(TopicMessageEventDTO.TypeEnum.DONE)
@ -41,12 +41,12 @@ class ConsumingStats {
} }
private TopicMessageConsumingDTO createConsumingStats(FluxSink<TopicMessageEventDTO> sink, private TopicMessageConsumingDTO createConsumingStats(FluxSink<TopicMessageEventDTO> sink,
Number filterApplyErrors) { int filterApplyErrors) {
return new TopicMessageConsumingDTO() return new TopicMessageConsumingDTO()
.bytesConsumed(this.bytes) .bytesConsumed(this.bytes)
.elapsedMs(this.elapsed) .elapsedMs(this.elapsed)
.isCancelled(sink.isCancelled()) .isCancelled(sink.isCancelled())
.filterApplyErrors(filterApplyErrors.intValue()) .filterApplyErrors(filterApplyErrors)
.messagesConsumed(this.records); .messagesConsumed(this.records);
} }
} }

View file

@ -2,7 +2,6 @@ package com.provectus.kafka.ui.emitter;
import com.provectus.kafka.ui.model.ConsumerPosition; import com.provectus.kafka.ui.model.ConsumerPosition;
import com.provectus.kafka.ui.model.TopicMessageEventDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO;
import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer;
import java.util.function.Supplier; import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecord;
@ -23,9 +22,9 @@ public class ForwardRecordEmitter
public ForwardRecordEmitter( public ForwardRecordEmitter(
Supplier<KafkaConsumer<Bytes, Bytes>> consumerSupplier, Supplier<KafkaConsumer<Bytes, Bytes>> consumerSupplier,
ConsumerPosition position, ConsumerPosition position,
ConsumerRecordDeserializer recordDeserializer, MessagesProcessing messagesProcessing,
PollingSettings pollingSettings) { PollingSettings pollingSettings) {
super(recordDeserializer, pollingSettings); super(messagesProcessing, pollingSettings);
this.position = position; this.position = position;
this.consumerSupplier = consumerSupplier; this.consumerSupplier = consumerSupplier;
} }
@ -40,6 +39,7 @@ public class ForwardRecordEmitter
EmptyPollsCounter emptyPolls = pollingSettings.createEmptyPollsCounter(); EmptyPollsCounter emptyPolls = pollingSettings.createEmptyPollsCounter();
while (!sink.isCancelled() while (!sink.isCancelled()
&& !sendLimitReached()
&& !seekOperations.assignedPartitionsFullyPolled() && !seekOperations.assignedPartitionsFullyPolled()
&& !emptyPolls.noDataEmptyPollsReached()) { && !emptyPolls.noDataEmptyPollsReached()) {
@ -50,11 +50,7 @@ public class ForwardRecordEmitter
log.debug("{} records polled", records.count()); log.debug("{} records polled", records.count());
for (ConsumerRecord<Bytes, Bytes> msg : records) { for (ConsumerRecord<Bytes, Bytes> msg : records) {
if (!sink.isCancelled()) { sendMessage(sink, msg);
sendMessage(sink, msg);
} else {
break;
}
} }
} }
sendFinishStatsAndCompleteSink(sink); sendFinishStatsAndCompleteSink(sink);

View file

@ -1,16 +0,0 @@
package com.provectus.kafka.ui.emitter;
import java.util.concurrent.atomic.AtomicLong;
import lombok.AccessLevel;
import lombok.Getter;
public class MessageFilterStats {
@Getter(AccessLevel.PACKAGE)
private final AtomicLong filterApplyErrors = new AtomicLong();
public final void incrementApplyErrors() {
filterApplyErrors.incrementAndGet();
}
}

View file

@ -0,0 +1,82 @@
package com.provectus.kafka.ui.emitter;
import com.provectus.kafka.ui.model.TopicMessageDTO;
import com.provectus.kafka.ui.model.TopicMessageEventDTO;
import com.provectus.kafka.ui.model.TopicMessagePhaseDTO;
import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer;
import java.util.function.Predicate;
import javax.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.common.utils.Bytes;
import reactor.core.publisher.FluxSink;
@Slf4j
public class MessagesProcessing {
private final ConsumingStats consumingStats = new ConsumingStats();
private long sentMessages = 0;
private int filterApplyErrors = 0;
private final ConsumerRecordDeserializer deserializer;
private final Predicate<TopicMessageDTO> filter;
private final @Nullable Integer limit;
public MessagesProcessing(ConsumerRecordDeserializer deserializer,
Predicate<TopicMessageDTO> filter,
@Nullable Integer limit) {
this.deserializer = deserializer;
this.filter = filter;
this.limit = limit;
}
boolean limitReached() {
return limit != null && sentMessages >= limit;
}
void sendMsg(FluxSink<TopicMessageEventDTO> sink, ConsumerRecord<Bytes, Bytes> rec) {
if (!sink.isCancelled() && !limitReached()) {
TopicMessageDTO topicMessage = deserializer.deserialize(rec);
try {
if (filter.test(topicMessage)) {
sink.next(
new TopicMessageEventDTO()
.type(TopicMessageEventDTO.TypeEnum.MESSAGE)
.message(topicMessage)
);
sentMessages++;
}
} catch (Exception e) {
filterApplyErrors++;
log.trace("Error applying filter for message {}", topicMessage);
}
}
}
int sentConsumingInfo(FluxSink<TopicMessageEventDTO> sink,
ConsumerRecords<Bytes, Bytes> polledRecords,
long elapsed) {
if (!sink.isCancelled()) {
return consumingStats.sendConsumingEvt(sink, polledRecords, elapsed, filterApplyErrors);
}
return 0;
}
void sendFinishEvent(FluxSink<TopicMessageEventDTO> sink) {
if (!sink.isCancelled()) {
consumingStats.sendFinishEvent(sink, filterApplyErrors);
}
}
void sendPhase(FluxSink<TopicMessageEventDTO> sink, String name) {
if (!sink.isCancelled()) {
sink.next(
new TopicMessageEventDTO()
.type(TopicMessageEventDTO.TypeEnum.PHASE)
.phase(new TopicMessagePhaseDTO().name(name))
);
}
}
}

View file

@ -2,7 +2,6 @@ package com.provectus.kafka.ui.emitter;
import com.provectus.kafka.ui.model.ConsumerPosition; import com.provectus.kafka.ui.model.ConsumerPosition;
import com.provectus.kafka.ui.model.TopicMessageEventDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO;
import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer;
import java.util.HashMap; import java.util.HashMap;
import java.util.function.Supplier; import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -20,9 +19,9 @@ public class TailingEmitter extends AbstractEmitter
public TailingEmitter(Supplier<KafkaConsumer<Bytes, Bytes>> consumerSupplier, public TailingEmitter(Supplier<KafkaConsumer<Bytes, Bytes>> consumerSupplier,
ConsumerPosition consumerPosition, ConsumerPosition consumerPosition,
ConsumerRecordDeserializer recordDeserializer, MessagesProcessing messagesProcessing,
PollingSettings pollingSettings) { PollingSettings pollingSettings) {
super(recordDeserializer, pollingSettings); super(messagesProcessing, pollingSettings);
this.consumerSupplier = consumerSupplier; this.consumerSupplier = consumerSupplier;
this.consumerPosition = consumerPosition; this.consumerPosition = consumerPosition;
} }

View file

@ -134,7 +134,7 @@ public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHan
.timestamp(currentTimestamp()) .timestamp(currentTimestamp())
.stackTrace(Throwables.getStackTraceAsString(exception)); .stackTrace(Throwables.getStackTraceAsString(exception));
return ServerResponse return ServerResponse
.status(exception.getStatus()) .status(exception.getStatusCode())
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.bodyValue(response); .bodyValue(response);
} }

View file

@ -1,5 +1,6 @@
package com.provectus.kafka.ui.model.rbac; package com.provectus.kafka.ui.model.rbac;
import static com.provectus.kafka.ui.model.rbac.Resource.APPLICATIONCONFIG;
import static com.provectus.kafka.ui.model.rbac.Resource.CLUSTERCONFIG; import static com.provectus.kafka.ui.model.rbac.Resource.CLUSTERCONFIG;
import static com.provectus.kafka.ui.model.rbac.Resource.KSQL; import static com.provectus.kafka.ui.model.rbac.Resource.KSQL;
@ -26,6 +27,8 @@ import org.springframework.util.Assert;
@EqualsAndHashCode @EqualsAndHashCode
public class Permission { public class Permission {
private static final List<Resource> RBAC_ACTION_EXEMPT_LIST = List.of(KSQL, CLUSTERCONFIG, APPLICATIONCONFIG);
Resource resource; Resource resource;
List<String> actions; List<String> actions;
@ -51,7 +54,7 @@ public class Permission {
public void validate() { public void validate() {
Assert.notNull(resource, "resource cannot be null"); Assert.notNull(resource, "resource cannot be null");
if (!List.of(KSQL, CLUSTERCONFIG).contains(this.resource)) { if (!RBAC_ACTION_EXEMPT_LIST.contains(this.resource)) {
Assert.notNull(value, "permission value can't be empty for resource " + resource); Assert.notNull(value, "permission value can't be empty for resource " + resource);
} }
} }

View file

@ -0,0 +1,76 @@
package com.provectus.kafka.ui.service;
import static com.provectus.kafka.ui.model.ApplicationInfoDTO.EnabledFeaturesEnum;
import com.provectus.kafka.ui.model.ApplicationInfoBuildDTO;
import com.provectus.kafka.ui.model.ApplicationInfoDTO;
import com.provectus.kafka.ui.model.ApplicationInfoLatestReleaseDTO;
import com.provectus.kafka.ui.util.DynamicConfigOperations;
import com.provectus.kafka.ui.util.GithubReleaseInfo;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.info.BuildProperties;
import org.springframework.boot.info.GitProperties;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
public class ApplicationInfoService {
private final GithubReleaseInfo githubReleaseInfo = new GithubReleaseInfo();
private final DynamicConfigOperations dynamicConfigOperations;
private final BuildProperties buildProperties;
private final GitProperties gitProperties;
public ApplicationInfoService(DynamicConfigOperations dynamicConfigOperations,
@Autowired(required = false) BuildProperties buildProperties,
@Autowired(required = false) GitProperties gitProperties) {
this.dynamicConfigOperations = dynamicConfigOperations;
this.buildProperties = Optional.ofNullable(buildProperties).orElse(new BuildProperties(new Properties()));
this.gitProperties = Optional.ofNullable(gitProperties).orElse(new GitProperties(new Properties()));
}
public ApplicationInfoDTO getApplicationInfo() {
var releaseInfo = githubReleaseInfo.get();
return new ApplicationInfoDTO()
.build(getBuildInfo(releaseInfo))
.enabledFeatures(getEnabledFeatures())
.latestRelease(convert(releaseInfo));
}
private ApplicationInfoLatestReleaseDTO convert(GithubReleaseInfo.GithubReleaseDto releaseInfo) {
return new ApplicationInfoLatestReleaseDTO()
.htmlUrl(releaseInfo.html_url())
.publishedAt(releaseInfo.published_at())
.versionTag(releaseInfo.tag_name());
}
private ApplicationInfoBuildDTO getBuildInfo(GithubReleaseInfo.GithubReleaseDto release) {
return new ApplicationInfoBuildDTO()
.isLatestRelease(release.tag_name() != null && release.tag_name().equals(buildProperties.getVersion()))
.commitId(gitProperties.getShortCommitId())
.version(buildProperties.getVersion())
.buildTime(buildProperties.getTime() != null
? DateTimeFormatter.ISO_INSTANT.format(buildProperties.getTime()) : null);
}
private List<EnabledFeaturesEnum> getEnabledFeatures() {
var enabledFeatures = new ArrayList<EnabledFeaturesEnum>();
if (dynamicConfigOperations.dynamicConfigEnabled()) {
enabledFeatures.add(EnabledFeaturesEnum.DYNAMIC_CONFIG);
}
return enabledFeatures;
}
// updating on startup and every hour
@Scheduled(fixedRateString = "${github-release-info-update-rate:3600000}")
public void updateGithubReleaseInfo() {
githubReleaseInfo.refresh().block();
}
}

View file

@ -1,38 +1,58 @@
package com.provectus.kafka.ui.service; package com.provectus.kafka.ui.service;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import com.google.common.collect.ImmutableList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.kafka.common.config.ConfigDef; import org.apache.kafka.common.config.ConfigDef;
import org.apache.kafka.common.config.SaslConfigs; import org.apache.kafka.common.config.SaslConfigs;
import org.apache.kafka.common.config.SslConfigs; import org.apache.kafka.common.config.SslConfigs;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
class KafkaConfigSanitizer extends Sanitizer { class KafkaConfigSanitizer {
private static final List<String> DEFAULT_PATTERNS_TO_SANITIZE = Arrays.asList(
"basic.auth.user.info", /* For Schema Registry credentials */ private static final String SANITIZED_VALUE = "******";
"password", "secret", "token", "key", ".*credentials.*", /* General credential patterns */
"aws.access.*", "aws.secret.*", "aws.session.*" /* AWS-related credential patterns */ private static final String[] REGEX_PARTS = {"*", "$", "^", "+"};
);
private static final List<String> DEFAULT_PATTERNS_TO_SANITIZE = ImmutableList.<String>builder()
.addAll(kafkaConfigKeysToSanitize())
.add(
"basic.auth.user.info", /* For Schema Registry credentials */
"password", "secret", "token", "key", ".*credentials.*", /* General credential patterns */
"aws.access.*", "aws.secret.*", "aws.session.*" /* AWS-related credential patterns */
)
.build();
private final List<Pattern> sanitizeKeysPatterns;
KafkaConfigSanitizer( KafkaConfigSanitizer(
@Value("${kafka.config.sanitizer.enabled:true}") boolean enabled, @Value("${kafka.config.sanitizer.enabled:true}") boolean enabled,
@Value("${kafka.config.sanitizer.patterns:}") List<String> patternsToSanitize @Value("${kafka.config.sanitizer.patterns:}") List<String> patternsToSanitize
) { ) {
if (!enabled) { this.sanitizeKeysPatterns = enabled
setKeysToSanitize(); ? compile(patternsToSanitize.isEmpty() ? DEFAULT_PATTERNS_TO_SANITIZE : patternsToSanitize)
} else { : List.of();
var keysToSanitize = new HashSet<>( }
patternsToSanitize.isEmpty() ? DEFAULT_PATTERNS_TO_SANITIZE : patternsToSanitize);
keysToSanitize.addAll(kafkaConfigKeysToSanitize()); private static List<Pattern> compile(Collection<String> patternStrings) {
setKeysToSanitize(keysToSanitize.toArray(new String[] {})); return patternStrings.stream()
} .map(p -> isRegex(p)
? Pattern.compile(p, CASE_INSENSITIVE)
: Pattern.compile(".*" + p + "$", CASE_INSENSITIVE))
.toList();
}
private static boolean isRegex(String str) {
return Arrays.stream(REGEX_PARTS).anyMatch(str::contains);
} }
private static Set<String> kafkaConfigKeysToSanitize() { private static Set<String> kafkaConfigKeysToSanitize() {
@ -45,4 +65,17 @@ class KafkaConfigSanitizer extends Sanitizer {
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
public Object sanitize(String key, Object value) {
if (value == null) {
return null;
}
for (Pattern pattern : sanitizeKeysPatterns) {
if (pattern.matcher(key).matches()) {
return SANITIZED_VALUE;
}
}
return value;
}
} }

View file

@ -225,11 +225,11 @@ public class KafkaConnectService {
} }
public Mono<ConnectorDTO> setConnectorConfig(KafkaCluster cluster, String connectName, public Mono<ConnectorDTO> setConnectorConfig(KafkaCluster cluster, String connectName,
String connectorName, Mono<Object> requestBody) { String connectorName, Mono<Map<String, Object>> requestBody) {
return api(cluster, connectName) return api(cluster, connectName)
.mono(c -> .mono(c ->
requestBody requestBody
.flatMap(body -> c.setConnectorConfig(connectorName, (Map<String, Object>) body)) .flatMap(body -> c.setConnectorConfig(connectorName, body))
.map(kafkaConnectMapper::fromClient)); .map(kafkaConnectMapper::fromClient));
} }
@ -298,12 +298,12 @@ public class KafkaConnectService {
} }
public Mono<ConnectorPluginConfigValidationResponseDTO> validateConnectorPluginConfig( public Mono<ConnectorPluginConfigValidationResponseDTO> validateConnectorPluginConfig(
KafkaCluster cluster, String connectName, String pluginName, Mono<Object> requestBody) { KafkaCluster cluster, String connectName, String pluginName, Mono<Map<String, Object>> requestBody) {
return api(cluster, connectName) return api(cluster, connectName)
.mono(client -> .mono(client ->
requestBody requestBody
.flatMap(body -> .flatMap(body ->
client.validateConnectorPluginConfig(pluginName, (Map<String, Object>) body)) client.validateConnectorPluginConfig(pluginName, body))
.map(kafkaConnectMapper::fromClient) .map(kafkaConnectMapper::fromClient)
); );
} }

View file

@ -3,9 +3,8 @@ package com.provectus.kafka.ui.service;
import com.google.common.util.concurrent.RateLimiter; import com.google.common.util.concurrent.RateLimiter;
import com.provectus.kafka.ui.emitter.BackwardRecordEmitter; import com.provectus.kafka.ui.emitter.BackwardRecordEmitter;
import com.provectus.kafka.ui.emitter.ForwardRecordEmitter; import com.provectus.kafka.ui.emitter.ForwardRecordEmitter;
import com.provectus.kafka.ui.emitter.MessageFilterStats;
import com.provectus.kafka.ui.emitter.MessageFilters; import com.provectus.kafka.ui.emitter.MessageFilters;
import com.provectus.kafka.ui.emitter.ResultSizeLimiter; import com.provectus.kafka.ui.emitter.MessagesProcessing;
import com.provectus.kafka.ui.emitter.TailingEmitter; import com.provectus.kafka.ui.emitter.TailingEmitter;
import com.provectus.kafka.ui.exception.TopicNotFoundException; import com.provectus.kafka.ui.exception.TopicNotFoundException;
import com.provectus.kafka.ui.exception.ValidationException; import com.provectus.kafka.ui.exception.ValidationException;
@ -14,9 +13,9 @@ import com.provectus.kafka.ui.model.CreateTopicMessageDTO;
import com.provectus.kafka.ui.model.KafkaCluster; import com.provectus.kafka.ui.model.KafkaCluster;
import com.provectus.kafka.ui.model.MessageFilterTypeDTO; import com.provectus.kafka.ui.model.MessageFilterTypeDTO;
import com.provectus.kafka.ui.model.SeekDirectionDTO; import com.provectus.kafka.ui.model.SeekDirectionDTO;
import com.provectus.kafka.ui.model.TopicMessageDTO;
import com.provectus.kafka.ui.model.TopicMessageEventDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO;
import com.provectus.kafka.ui.serde.api.Serde; 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.serdes.ProducerRecordCreator;
import com.provectus.kafka.ui.util.SslPropertiesUtil; import com.provectus.kafka.ui.util.SslPropertiesUtil;
import java.util.List; import java.util.List;
@ -162,13 +161,18 @@ public class MessagesService {
@Nullable String valueSerde) { @Nullable String valueSerde) {
java.util.function.Consumer<? super FluxSink<TopicMessageEventDTO>> emitter; java.util.function.Consumer<? super FluxSink<TopicMessageEventDTO>> emitter;
ConsumerRecordDeserializer recordDeserializer =
deserializationService.deserializerFor(cluster, topic, keySerde, valueSerde); var processing = new MessagesProcessing(
deserializationService.deserializerFor(cluster, topic, keySerde, valueSerde),
getMsgFilter(query, filterQueryType),
seekDirection == SeekDirectionDTO.TAILING ? null : limit
);
if (seekDirection.equals(SeekDirectionDTO.FORWARD)) { if (seekDirection.equals(SeekDirectionDTO.FORWARD)) {
emitter = new ForwardRecordEmitter( emitter = new ForwardRecordEmitter(
() -> consumerGroupService.createConsumer(cluster), () -> consumerGroupService.createConsumer(cluster),
consumerPosition, consumerPosition,
recordDeserializer, processing,
cluster.getPollingSettings() cluster.getPollingSettings()
); );
} else if (seekDirection.equals(SeekDirectionDTO.BACKWARD)) { } else if (seekDirection.equals(SeekDirectionDTO.BACKWARD)) {
@ -176,33 +180,22 @@ public class MessagesService {
() -> consumerGroupService.createConsumer(cluster), () -> consumerGroupService.createConsumer(cluster),
consumerPosition, consumerPosition,
limit, limit,
recordDeserializer, processing,
cluster.getPollingSettings() cluster.getPollingSettings()
); );
} else { } else {
emitter = new TailingEmitter( emitter = new TailingEmitter(
() -> consumerGroupService.createConsumer(cluster), () -> consumerGroupService.createConsumer(cluster),
consumerPosition, consumerPosition,
recordDeserializer, processing,
cluster.getPollingSettings() cluster.getPollingSettings()
); );
} }
MessageFilterStats filterStats = new MessageFilterStats();
return Flux.create(emitter) return Flux.create(emitter)
.contextWrite(ctx -> ctx.put(MessageFilterStats.class, filterStats))
.filter(getMsgFilter(query, filterQueryType, filterStats))
.map(getDataMasker(cluster, topic)) .map(getDataMasker(cluster, topic))
.takeWhile(createTakeWhilePredicate(seekDirection, limit))
.map(throttleUiPublish(seekDirection)); .map(throttleUiPublish(seekDirection));
} }
private Predicate<TopicMessageEventDTO> createTakeWhilePredicate(
SeekDirectionDTO seekDirection, int limit) {
return seekDirection == SeekDirectionDTO.TAILING
? evt -> true // no limit for tailing
: new ResultSizeLimiter(limit);
}
private UnaryOperator<TopicMessageEventDTO> getDataMasker(KafkaCluster cluster, String topicName) { private UnaryOperator<TopicMessageEventDTO> getDataMasker(KafkaCluster cluster, String topicName) {
var keyMasker = cluster.getMasking().getMaskingFunction(topicName, Serde.Target.KEY); var keyMasker = cluster.getMasking().getMaskingFunction(topicName, Serde.Target.KEY);
var valMasker = cluster.getMasking().getMaskingFunction(topicName, Serde.Target.VALUE); var valMasker = cluster.getMasking().getMaskingFunction(topicName, Serde.Target.VALUE);
@ -211,32 +204,18 @@ public class MessagesService {
return evt; return evt;
} }
return evt.message( return evt.message(
evt.getMessage() evt.getMessage()
.key(keyMasker.apply(evt.getMessage().getKey())) .key(keyMasker.apply(evt.getMessage().getKey()))
.content(valMasker.apply(evt.getMessage().getContent()))); .content(valMasker.apply(evt.getMessage().getContent())));
}; };
} }
private Predicate<TopicMessageEventDTO> getMsgFilter(String query, private Predicate<TopicMessageDTO> getMsgFilter(String query,
MessageFilterTypeDTO filterQueryType, MessageFilterTypeDTO filterQueryType) {
MessageFilterStats filterStats) {
if (StringUtils.isEmpty(query)) { if (StringUtils.isEmpty(query)) {
return evt -> true; return evt -> true;
} }
var messageFilter = MessageFilters.createMsgFilter(query, filterQueryType); return MessageFilters.createMsgFilter(query, filterQueryType);
return evt -> {
// we only apply filter for message events
if (evt.getType() == TopicMessageEventDTO.TypeEnum.MESSAGE) {
try {
return messageFilter.test(evt.getMessage());
} catch (Exception e) {
filterStats.incrementApplyErrors();
log.trace("Error applying filter '{}' for message {}", query, evt.getMessage());
return false;
}
}
return true;
};
} }
private <T> UnaryOperator<T> throttleUiPublish(SeekDirectionDTO seekDirection) { private <T> UnaryOperator<T> throttleUiPublish(SeekDirectionDTO seekDirection) {

View file

@ -4,6 +4,7 @@ import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toMap;
import static org.apache.kafka.clients.admin.ListOffsetsResult.ListOffsetsResultInfo; import static org.apache.kafka.clients.admin.ListOffsetsResult.ListOffsetsResultInfo;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableTable; import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@ -514,6 +515,14 @@ public class ReactiveAdminClient implements Closeable {
.flatMap(parts -> listOffsetsUnsafe(parts, offsetSpec)); .flatMap(parts -> listOffsetsUnsafe(parts, offsetSpec));
} }
/**
* List offset for the specified topics, skipping no-leader partitions.
*/
public Mono<Map<TopicPartition, Long>> listOffsets(Collection<TopicDescription> topicDescriptions,
OffsetSpec offsetSpec) {
return listOffsetsUnsafe(filterPartitionsWithLeaderCheck(topicDescriptions, p -> true, false), offsetSpec);
}
private Mono<Collection<TopicPartition>> filterPartitionsWithLeaderCheck(Collection<TopicPartition> partitions, private Mono<Collection<TopicPartition>> filterPartitionsWithLeaderCheck(Collection<TopicPartition> partitions,
boolean failOnUnknownLeader) { boolean failOnUnknownLeader) {
var targetTopics = partitions.stream().map(TopicPartition::topic).collect(Collectors.toSet()); var targetTopics = partitions.stream().map(TopicPartition::topic).collect(Collectors.toSet());
@ -523,34 +532,44 @@ public class ReactiveAdminClient implements Closeable {
descriptions.values(), partitions::contains, failOnUnknownLeader)); descriptions.values(), partitions::contains, failOnUnknownLeader));
} }
private Set<TopicPartition> filterPartitionsWithLeaderCheck(Collection<TopicDescription> topicDescriptions, @VisibleForTesting
static Set<TopicPartition> filterPartitionsWithLeaderCheck(Collection<TopicDescription> topicDescriptions,
Predicate<TopicPartition> partitionPredicate, Predicate<TopicPartition> partitionPredicate,
boolean failOnUnknownLeader) { boolean failOnUnknownLeader) {
var goodPartitions = new HashSet<TopicPartition>(); var goodPartitions = new HashSet<TopicPartition>();
for (TopicDescription description : topicDescriptions) { for (TopicDescription description : topicDescriptions) {
var goodTopicPartitions = new ArrayList<TopicPartition>();
for (TopicPartitionInfo partitionInfo : description.partitions()) { for (TopicPartitionInfo partitionInfo : description.partitions()) {
TopicPartition topicPartition = new TopicPartition(description.name(), partitionInfo.partition()); TopicPartition topicPartition = new TopicPartition(description.name(), partitionInfo.partition());
if (!partitionPredicate.test(topicPartition)) { if (partitionInfo.leader() == null) {
continue; if (failOnUnknownLeader) {
throw new ValidationException(String.format("Topic partition %s has no leader", topicPartition));
} else {
// if ANY of topic partitions has no leader - we have to skip all topic partitions
goodTopicPartitions.clear();
break;
}
} }
if (partitionInfo.leader() != null) { if (partitionPredicate.test(topicPartition)) {
goodPartitions.add(topicPartition); goodTopicPartitions.add(topicPartition);
} else if (failOnUnknownLeader) {
throw new ValidationException(String.format("Topic partition %s has no leader", topicPartition));
} }
} }
goodPartitions.addAll(goodTopicPartitions);
} }
return goodPartitions; return goodPartitions;
} }
// 1. NOTE(!): should only apply for partitions with existing leader, // 1. NOTE(!): should only apply for partitions from topics where all partitions have leaders,
// otherwise AdminClient will try to fetch topic metadata, fail and retry infinitely (until timeout) // otherwise AdminClient will try to fetch topic metadata, fail and retry infinitely (until timeout)
// 2. NOTE(!): Skips partitions that were not initialized yet // 2. NOTE(!): Skips partitions that were not initialized yet
// (UnknownTopicOrPartitionException thrown, ex. after topic creation) // (UnknownTopicOrPartitionException thrown, ex. after topic creation)
// 3. TODO: check if it is a bug that AdminClient never throws LeaderNotAvailableException and just retrying instead // 3. TODO: check if it is a bug that AdminClient never throws LeaderNotAvailableException and just retrying instead
@KafkaClientInternalsDependant @KafkaClientInternalsDependant
public Mono<Map<TopicPartition, Long>> listOffsetsUnsafe(Collection<TopicPartition> partitions, @VisibleForTesting
OffsetSpec offsetSpec) { Mono<Map<TopicPartition, Long>> listOffsetsUnsafe(Collection<TopicPartition> partitions, OffsetSpec offsetSpec) {
if (partitions.isEmpty()) {
return Mono.just(Map.of());
}
Function<Collection<TopicPartition>, Mono<Map<TopicPartition, Long>>> call = Function<Collection<TopicPartition>, Mono<Map<TopicPartition, Long>>> call =
parts -> { parts -> {

View file

@ -3,6 +3,7 @@ package com.provectus.kafka.ui.service;
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toMap;
import com.google.common.collect.Sets;
import com.provectus.kafka.ui.config.ClustersProperties; import com.provectus.kafka.ui.config.ClustersProperties;
import com.provectus.kafka.ui.exception.TopicMetadataException; import com.provectus.kafka.ui.exception.TopicMetadataException;
import com.provectus.kafka.ui.exception.TopicNotFoundException; import com.provectus.kafka.ui.exception.TopicNotFoundException;
@ -136,22 +137,14 @@ public class TopicsService {
} }
private Mono<InternalPartitionsOffsets> getPartitionOffsets(Map<String, TopicDescription> private Mono<InternalPartitionsOffsets> getPartitionOffsets(Map<String, TopicDescription>
descriptions, descriptionsMap,
ReactiveAdminClient ac) { ReactiveAdminClient ac) {
var topicPartitions = descriptions.values().stream() var descriptions = descriptionsMap.values();
.flatMap(desc -> return ac.listOffsets(descriptions, OffsetSpec.earliest())
desc.partitions().stream() .zipWith(ac.listOffsets(descriptions, OffsetSpec.latest()),
// list offsets should only be applied to partitions with existing leader
// (see ReactiveAdminClient.listOffsetsUnsafe(..) docs)
.filter(tp -> tp.leader() != null)
.map(p -> new TopicPartition(desc.name(), p.partition())))
.collect(toList());
return ac.listOffsetsUnsafe(topicPartitions, OffsetSpec.earliest())
.zipWith(ac.listOffsetsUnsafe(topicPartitions, OffsetSpec.latest()),
(earliest, latest) -> (earliest, latest) ->
topicPartitions.stream() Sets.intersection(earliest.keySet(), latest.keySet())
.filter(tp -> earliest.containsKey(tp) && latest.containsKey(tp)) .stream()
.map(tp -> .map(tp ->
Map.entry(tp, Map.entry(tp,
new InternalPartitionsOffsets.Offsets( new InternalPartitionsOffsets.Offsets(

View file

@ -2,7 +2,7 @@ package com.provectus.kafka.ui.service.analyze;
import com.provectus.kafka.ui.model.TopicAnalysisSizeStatsDTO; import com.provectus.kafka.ui.model.TopicAnalysisSizeStatsDTO;
import com.provectus.kafka.ui.model.TopicAnalysisStatsDTO; import com.provectus.kafka.ui.model.TopicAnalysisStatsDTO;
import com.provectus.kafka.ui.model.TopicAnalysisStatsHourlyMsgCountsDTO; import com.provectus.kafka.ui.model.TopicAnalysisStatsHourlyMsgCountsInnerDTO;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Comparator; import java.util.Comparator;
@ -78,10 +78,10 @@ class TopicAnalysisStats {
} }
} }
List<TopicAnalysisStatsHourlyMsgCountsDTO> toDto() { List<TopicAnalysisStatsHourlyMsgCountsInnerDTO> toDto() {
return hourlyStats.entrySet().stream() return hourlyStats.entrySet().stream()
.sorted(Comparator.comparingLong(Map.Entry::getKey)) .sorted(Comparator.comparingLong(Map.Entry::getKey))
.map(e -> new TopicAnalysisStatsHourlyMsgCountsDTO() .map(e -> new TopicAnalysisStatsHourlyMsgCountsInnerDTO()
.hourStart(e.getKey()) .hourStart(e.getKey())
.count(e.getValue())) .count(e.getValue()))
.collect(Collectors.toList()); .collect(Collectors.toList());

View file

@ -52,7 +52,10 @@ public class KsqlApiClient {
boolean error; boolean error;
public Optional<JsonNode> getColumnValue(List<JsonNode> row, String column) { public Optional<JsonNode> getColumnValue(List<JsonNode> row, String column) {
return Optional.ofNullable(row.get(columnNames.indexOf(column))); int colIdx = columnNames.indexOf(column);
return colIdx >= 0
? Optional.ofNullable(row.get(colIdx))
: Optional.empty();
} }
} }

View file

@ -89,7 +89,14 @@ public class KsqlServiceV2 {
.name(resp.getColumnValue(row, "name").map(JsonNode::asText).orElse(null)) .name(resp.getColumnValue(row, "name").map(JsonNode::asText).orElse(null))
.topic(resp.getColumnValue(row, "topic").map(JsonNode::asText).orElse(null)) .topic(resp.getColumnValue(row, "topic").map(JsonNode::asText).orElse(null))
.keyFormat(resp.getColumnValue(row, "keyFormat").map(JsonNode::asText).orElse(null)) .keyFormat(resp.getColumnValue(row, "keyFormat").map(JsonNode::asText).orElse(null))
.valueFormat(resp.getColumnValue(row, "valueFormat").map(JsonNode::asText).orElse(null))) .valueFormat(
// for old versions (<0.13) "format" column is filled,
// for new version "keyFormat" & "valueFormat" columns should be filled
resp.getColumnValue(row, "valueFormat")
.or(() -> resp.getColumnValue(row, "format"))
.map(JsonNode::asText)
.orElse(null))
)
.collect(Collectors.toList())); .collect(Collectors.toList()));
}); });
} }

View file

@ -21,6 +21,7 @@ import com.provectus.kafka.ui.service.rbac.extractor.GithubAuthorityExtractor;
import com.provectus.kafka.ui.service.rbac.extractor.GoogleAuthorityExtractor; import com.provectus.kafka.ui.service.rbac.extractor.GoogleAuthorityExtractor;
import com.provectus.kafka.ui.service.rbac.extractor.LdapAuthorityExtractor; import com.provectus.kafka.ui.service.rbac.extractor.LdapAuthorityExtractor;
import com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor; import com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor;
import jakarta.annotation.PostConstruct;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -28,7 +29,6 @@ import java.util.function.Predicate;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;

View file

@ -1,9 +1,9 @@
package com.provectus.kafka.ui.service.rbac.extractor; package com.provectus.kafka.ui.service.rbac.extractor;
import com.nimbusds.jose.shaded.json.JSONArray;
import com.provectus.kafka.ui.model.rbac.Role; import com.provectus.kafka.ui.model.rbac.Role;
import com.provectus.kafka.ui.model.rbac.provider.Provider; import com.provectus.kafka.ui.model.rbac.provider.Provider;
import com.provectus.kafka.ui.service.rbac.AccessControlService; import com.provectus.kafka.ui.service.rbac.AccessControlService;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -44,7 +44,7 @@ public class CognitoAuthorityExtractor implements ProviderAuthorityExtractor {
.map(Role::getName) .map(Role::getName)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
JSONArray groups = principal.getAttribute(COGNITO_GROUPS_ATTRIBUTE_NAME); List<String> groups = principal.getAttribute(COGNITO_GROUPS_ATTRIBUTE_NAME);
if (groups == null) { if (groups == null) {
log.debug("Cognito groups param is not present"); log.debug("Cognito groups param is not present");
return Mono.just(groupsByUsername); return Mono.just(groupsByUsername);
@ -56,9 +56,8 @@ public class CognitoAuthorityExtractor implements ProviderAuthorityExtractor {
.stream() .stream()
.filter(s -> s.getProvider().equals(Provider.OAUTH_COGNITO)) .filter(s -> s.getProvider().equals(Provider.OAUTH_COGNITO))
.filter(s -> s.getType().equals("group")) .filter(s -> s.getType().equals("group"))
.anyMatch(subject -> Stream.of(groups.toArray()) .anyMatch(subject -> Stream.of(groups)
.map(Object::toString) .map(Object::toString)
.distinct()
.anyMatch(cognitoGroup -> cognitoGroup.equals(subject.getValue())) .anyMatch(cognitoGroup -> cognitoGroup.equals(subject.getValue()))
)) ))
.map(Role::getName) .map(Role::getName)

View file

@ -0,0 +1,53 @@
package com.provectus.kafka.ui.util;
import com.google.common.annotations.VisibleForTesting;
import java.time.Duration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
@Slf4j
public class GithubReleaseInfo {
private static final String GITHUB_LATEST_RELEASE_RETRIEVAL_URL =
"https://api.github.com/repos/provectus/kafka-ui/releases/latest";
private static final Duration GITHUB_API_MAX_WAIT_TIME = Duration.ofSeconds(2);
public record GithubReleaseDto(String html_url, String tag_name, String published_at) {
static GithubReleaseDto empty() {
return new GithubReleaseDto(null, null, null);
}
}
private volatile GithubReleaseDto release = GithubReleaseDto.empty();
private final Mono<Void> refreshMono;
public GithubReleaseInfo() {
this(GITHUB_LATEST_RELEASE_RETRIEVAL_URL);
}
@VisibleForTesting
GithubReleaseInfo(String url) {
this.refreshMono = WebClient.create()
.get()
.uri(url)
.exchangeToMono(resp -> resp.bodyToMono(GithubReleaseDto.class))
.timeout(GITHUB_API_MAX_WAIT_TIME)
.doOnError(th -> log.trace("Error getting latest github release info", th))
.onErrorResume(th -> true, th -> Mono.just(GithubReleaseDto.empty()))
.doOnNext(release -> this.release = release)
.then();
}
public GithubReleaseDto get() {
return release;
}
public Mono<Void> refresh() {
return refreshMono;
}
}

View file

@ -16,7 +16,7 @@ import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.springframework.util.SocketUtils; import org.springframework.test.util.TestSocketUtils;
import org.testcontainers.containers.KafkaContainer; import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.containers.Network; import org.testcontainers.containers.Network;
import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.DockerImageName;
@ -61,7 +61,7 @@ public abstract class AbstractIntegrationTest {
System.setProperty("kafka.clusters.0.bootstrapServers", kafka.getBootstrapServers()); System.setProperty("kafka.clusters.0.bootstrapServers", kafka.getBootstrapServers());
// List unavailable hosts to verify failover // List unavailable hosts to verify failover
System.setProperty("kafka.clusters.0.schemaRegistry", String.format("http://localhost:%1$s,http://localhost:%1$s,%2$s", System.setProperty("kafka.clusters.0.schemaRegistry", String.format("http://localhost:%1$s,http://localhost:%1$s,%2$s",
SocketUtils.findAvailableTcpPort(), schemaRegistry.getUrl())); TestSocketUtils.findAvailableTcpPort(), schemaRegistry.getUrl()));
System.setProperty("kafka.clusters.0.kafkaConnect.0.name", "kafka-connect"); System.setProperty("kafka.clusters.0.kafkaConnect.0.name", "kafka-connect");
System.setProperty("kafka.clusters.0.kafkaConnect.0.userName", "kafka-connect"); System.setProperty("kafka.clusters.0.kafkaConnect.0.userName", "kafka-connect");
System.setProperty("kafka.clusters.0.kafkaConnect.0.password", "kafka-connect"); System.setProperty("kafka.clusters.0.kafkaConnect.0.password", "kafka-connect");

View file

@ -5,13 +5,12 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.Sanitizer;
class KafkaConfigSanitizerTest { class KafkaConfigSanitizerTest {
@Test @Test
void doNothingIfEnabledPropertySetToFalse() { void doNothingIfEnabledPropertySetToFalse() {
final Sanitizer sanitizer = new KafkaConfigSanitizer(false, Collections.emptyList()); final var sanitizer = new KafkaConfigSanitizer(false, Collections.emptyList());
assertThat(sanitizer.sanitize("password", "secret")).isEqualTo("secret"); assertThat(sanitizer.sanitize("password", "secret")).isEqualTo("secret");
assertThat(sanitizer.sanitize("sasl.jaas.config", "secret")).isEqualTo("secret"); assertThat(sanitizer.sanitize("sasl.jaas.config", "secret")).isEqualTo("secret");
assertThat(sanitizer.sanitize("database.password", "secret")).isEqualTo("secret"); assertThat(sanitizer.sanitize("database.password", "secret")).isEqualTo("secret");
@ -19,7 +18,7 @@ class KafkaConfigSanitizerTest {
@Test @Test
void obfuscateCredentials() { void obfuscateCredentials() {
final Sanitizer sanitizer = new KafkaConfigSanitizer(true, Collections.emptyList()); final var sanitizer = new KafkaConfigSanitizer(true, Collections.emptyList());
assertThat(sanitizer.sanitize("sasl.jaas.config", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("sasl.jaas.config", "secret")).isEqualTo("******");
assertThat(sanitizer.sanitize("consumer.sasl.jaas.config", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("consumer.sasl.jaas.config", "secret")).isEqualTo("******");
assertThat(sanitizer.sanitize("producer.sasl.jaas.config", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("producer.sasl.jaas.config", "secret")).isEqualTo("******");
@ -37,7 +36,7 @@ class KafkaConfigSanitizerTest {
@Test @Test
void notObfuscateNormalConfigs() { void notObfuscateNormalConfigs() {
final Sanitizer sanitizer = new KafkaConfigSanitizer(true, Collections.emptyList()); final var sanitizer = new KafkaConfigSanitizer(true, Collections.emptyList());
assertThat(sanitizer.sanitize("security.protocol", "SASL_SSL")).isEqualTo("SASL_SSL"); assertThat(sanitizer.sanitize("security.protocol", "SASL_SSL")).isEqualTo("SASL_SSL");
final String[] bootstrapServer = new String[] {"test1:9092", "test2:9092"}; final String[] bootstrapServer = new String[] {"test1:9092", "test2:9092"};
assertThat(sanitizer.sanitize("bootstrap.servers", bootstrapServer)).isEqualTo(bootstrapServer); assertThat(sanitizer.sanitize("bootstrap.servers", bootstrapServer)).isEqualTo(bootstrapServer);
@ -45,7 +44,7 @@ class KafkaConfigSanitizerTest {
@Test @Test
void obfuscateCredentialsWithDefinedPatterns() { void obfuscateCredentialsWithDefinedPatterns() {
final Sanitizer sanitizer = new KafkaConfigSanitizer(true, Arrays.asList("kafka.ui", ".*test.*")); final var sanitizer = new KafkaConfigSanitizer(true, Arrays.asList("kafka.ui", ".*test.*"));
assertThat(sanitizer.sanitize("consumer.kafka.ui", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("consumer.kafka.ui", "secret")).isEqualTo("******");
assertThat(sanitizer.sanitize("this.is.test.credentials", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("this.is.test.credentials", "secret")).isEqualTo("******");
assertThat(sanitizer.sanitize("this.is.not.credential", "not.credential")) assertThat(sanitizer.sanitize("this.is.not.credential", "not.credential"))

View file

@ -4,8 +4,11 @@ import static com.provectus.kafka.ui.service.ReactiveAdminClient.toMonoWithExcep
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static org.apache.kafka.clients.admin.ListOffsetsResult.ListOffsetsResultInfo; import static org.apache.kafka.clients.admin.ListOffsetsResult.ListOffsetsResultInfo;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import com.provectus.kafka.ui.AbstractIntegrationTest; import com.provectus.kafka.ui.AbstractIntegrationTest;
import com.provectus.kafka.ui.exception.ValidationException;
import com.provectus.kafka.ui.producer.KafkaTestProducer; import com.provectus.kafka.ui.producer.KafkaTestProducer;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
@ -22,16 +25,20 @@ import org.apache.kafka.clients.admin.Config;
import org.apache.kafka.clients.admin.ConfigEntry; import org.apache.kafka.clients.admin.ConfigEntry;
import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.clients.admin.OffsetSpec; import org.apache.kafka.clients.admin.OffsetSpec;
import org.apache.kafka.clients.admin.TopicDescription;
import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.consumer.OffsetAndMetadata; import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.KafkaFuture; import org.apache.kafka.common.KafkaFuture;
import org.apache.kafka.common.Node;
import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.TopicPartitionInfo;
import org.apache.kafka.common.config.ConfigResource; import org.apache.kafka.common.config.ConfigResource;
import org.apache.kafka.common.errors.UnknownTopicOrPartitionException; import org.apache.kafka.common.errors.UnknownTopicOrPartitionException;
import org.apache.kafka.common.internals.KafkaFutureImpl; import org.apache.kafka.common.internals.KafkaFutureImpl;
import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringDeserializer;
import org.assertj.core.api.ThrowableAssert;
import org.junit.function.ThrowingRunnable; import org.junit.function.ThrowingRunnable;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -133,6 +140,56 @@ class ReactiveAdminClientTest extends AbstractIntegrationTest {
.verifyComplete(); .verifyComplete();
} }
@Test
void filterPartitionsWithLeaderCheckSkipsPartitionsFromTopicWhereSomePartitionsHaveNoLeader() {
var filteredPartitions = ReactiveAdminClient.filterPartitionsWithLeaderCheck(
List.of(
// contains partitions with no leader
new TopicDescription("noLeaderTopic", false,
List.of(
new TopicPartitionInfo(0, new Node(1, "n1", 9092), List.of(), List.of()),
new TopicPartitionInfo(1, null, List.of(), List.of()))),
// should be skipped by predicate
new TopicDescription("skippingByPredicate", false,
List.of(
new TopicPartitionInfo(0, new Node(1, "n1", 9092), List.of(), List.of()))),
// good topic
new TopicDescription("good", false,
List.of(
new TopicPartitionInfo(0, new Node(1, "n1", 9092), List.of(), List.of()),
new TopicPartitionInfo(1, new Node(2, "n2", 9092), List.of(), List.of()))
)),
p -> !p.topic().equals("skippingByPredicate"),
false
);
assertThat(filteredPartitions)
.containsExactlyInAnyOrder(
new TopicPartition("good", 0),
new TopicPartition("good", 1)
);
}
@Test
void filterPartitionsWithLeaderCheckThrowExceptionIfThereIsSomePartitionsWithoutLeaderAndFlagSet() {
ThrowingCallable call = () -> ReactiveAdminClient.filterPartitionsWithLeaderCheck(
List.of(
// contains partitions with no leader
new TopicDescription("t1", false,
List.of(
new TopicPartitionInfo(0, new Node(1, "n1", 9092), List.of(), List.of()),
new TopicPartitionInfo(1, null, List.of(), List.of()))),
new TopicDescription("t2", false,
List.of(
new TopicPartitionInfo(0, new Node(1, "n1", 9092), List.of(), List.of()))
)),
p -> true,
// setting failOnNoLeader flag
true
);
assertThatThrownBy(call).isInstanceOf(ValidationException.class);
}
@Test @Test
void testListOffsetsUnsafe() { void testListOffsetsUnsafe() {
String topic = UUID.randomUUID().toString(); String topic = UUID.randomUUID().toString();

View file

@ -9,6 +9,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import com.provectus.kafka.ui.AbstractIntegrationTest; import com.provectus.kafka.ui.AbstractIntegrationTest;
import com.provectus.kafka.ui.emitter.BackwardRecordEmitter; import com.provectus.kafka.ui.emitter.BackwardRecordEmitter;
import com.provectus.kafka.ui.emitter.ForwardRecordEmitter; import com.provectus.kafka.ui.emitter.ForwardRecordEmitter;
import com.provectus.kafka.ui.emitter.MessagesProcessing;
import com.provectus.kafka.ui.emitter.PollingSettings; import com.provectus.kafka.ui.emitter.PollingSettings;
import com.provectus.kafka.ui.model.ConsumerPosition; import com.provectus.kafka.ui.model.ConsumerPosition;
import com.provectus.kafka.ui.model.TopicMessageEventDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO;
@ -106,12 +107,16 @@ class RecordEmitterTest extends AbstractIntegrationTest {
); );
} }
private MessagesProcessing createMessagesProcessing() {
return new MessagesProcessing(RECORD_DESERIALIZER, msg -> true, null);
}
@Test @Test
void pollNothingOnEmptyTopic() { void pollNothingOnEmptyTopic() {
var forwardEmitter = new ForwardRecordEmitter( var forwardEmitter = new ForwardRecordEmitter(
this::createConsumer, this::createConsumer,
new ConsumerPosition(BEGINNING, EMPTY_TOPIC, null), new ConsumerPosition(BEGINNING, EMPTY_TOPIC, null),
RECORD_DESERIALIZER, createMessagesProcessing(),
PollingSettings.createDefault() PollingSettings.createDefault()
); );
@ -119,7 +124,7 @@ class RecordEmitterTest extends AbstractIntegrationTest {
this::createConsumer, this::createConsumer,
new ConsumerPosition(BEGINNING, EMPTY_TOPIC, null), new ConsumerPosition(BEGINNING, EMPTY_TOPIC, null),
100, 100,
RECORD_DESERIALIZER, createMessagesProcessing(),
PollingSettings.createDefault() PollingSettings.createDefault()
); );
@ -141,7 +146,7 @@ class RecordEmitterTest extends AbstractIntegrationTest {
var forwardEmitter = new ForwardRecordEmitter( var forwardEmitter = new ForwardRecordEmitter(
this::createConsumer, this::createConsumer,
new ConsumerPosition(BEGINNING, TOPIC, null), new ConsumerPosition(BEGINNING, TOPIC, null),
RECORD_DESERIALIZER, createMessagesProcessing(),
PollingSettings.createDefault() PollingSettings.createDefault()
); );
@ -149,7 +154,7 @@ class RecordEmitterTest extends AbstractIntegrationTest {
this::createConsumer, this::createConsumer,
new ConsumerPosition(LATEST, TOPIC, null), new ConsumerPosition(LATEST, TOPIC, null),
PARTITIONS * MSGS_PER_PARTITION, PARTITIONS * MSGS_PER_PARTITION,
RECORD_DESERIALIZER, createMessagesProcessing(),
PollingSettings.createDefault() PollingSettings.createDefault()
); );
@ -170,7 +175,7 @@ class RecordEmitterTest extends AbstractIntegrationTest {
var forwardEmitter = new ForwardRecordEmitter( var forwardEmitter = new ForwardRecordEmitter(
this::createConsumer, this::createConsumer,
new ConsumerPosition(OFFSET, TOPIC, targetOffsets), new ConsumerPosition(OFFSET, TOPIC, targetOffsets),
RECORD_DESERIALIZER, createMessagesProcessing(),
PollingSettings.createDefault() PollingSettings.createDefault()
); );
@ -178,7 +183,7 @@ class RecordEmitterTest extends AbstractIntegrationTest {
this::createConsumer, this::createConsumer,
new ConsumerPosition(OFFSET, TOPIC, targetOffsets), new ConsumerPosition(OFFSET, TOPIC, targetOffsets),
PARTITIONS * MSGS_PER_PARTITION, PARTITIONS * MSGS_PER_PARTITION,
RECORD_DESERIALIZER, createMessagesProcessing(),
PollingSettings.createDefault() PollingSettings.createDefault()
); );
@ -215,7 +220,7 @@ class RecordEmitterTest extends AbstractIntegrationTest {
var forwardEmitter = new ForwardRecordEmitter( var forwardEmitter = new ForwardRecordEmitter(
this::createConsumer, this::createConsumer,
new ConsumerPosition(TIMESTAMP, TOPIC, targetTimestamps), new ConsumerPosition(TIMESTAMP, TOPIC, targetTimestamps),
RECORD_DESERIALIZER, createMessagesProcessing(),
PollingSettings.createDefault() PollingSettings.createDefault()
); );
@ -223,7 +228,7 @@ class RecordEmitterTest extends AbstractIntegrationTest {
this::createConsumer, this::createConsumer,
new ConsumerPosition(TIMESTAMP, TOPIC, targetTimestamps), new ConsumerPosition(TIMESTAMP, TOPIC, targetTimestamps),
PARTITIONS * MSGS_PER_PARTITION, PARTITIONS * MSGS_PER_PARTITION,
RECORD_DESERIALIZER, createMessagesProcessing(),
PollingSettings.createDefault() PollingSettings.createDefault()
); );
@ -254,7 +259,7 @@ class RecordEmitterTest extends AbstractIntegrationTest {
this::createConsumer, this::createConsumer,
new ConsumerPosition(OFFSET, TOPIC, targetOffsets), new ConsumerPosition(OFFSET, TOPIC, targetOffsets),
numMessages, numMessages,
RECORD_DESERIALIZER, createMessagesProcessing(),
PollingSettings.createDefault() PollingSettings.createDefault()
); );
@ -280,7 +285,7 @@ class RecordEmitterTest extends AbstractIntegrationTest {
this::createConsumer, this::createConsumer,
new ConsumerPosition(OFFSET, TOPIC, offsets), new ConsumerPosition(OFFSET, TOPIC, offsets),
100, 100,
RECORD_DESERIALIZER, createMessagesProcessing(),
PollingSettings.createDefault() PollingSettings.createDefault()
); );

View file

@ -15,7 +15,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.util.unit.DataSize;
import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.DockerImageName;
class KsqlServiceV2Test extends AbstractIntegrationTest { class KsqlServiceV2Test extends AbstractIntegrationTest {
@ -27,8 +26,6 @@ class KsqlServiceV2Test extends AbstractIntegrationTest {
private static final Set<String> STREAMS_TO_DELETE = new CopyOnWriteArraySet<>(); private static final Set<String> STREAMS_TO_DELETE = new CopyOnWriteArraySet<>();
private static final Set<String> TABLES_TO_DELETE = new CopyOnWriteArraySet<>(); private static final Set<String> TABLES_TO_DELETE = new CopyOnWriteArraySet<>();
private static final DataSize maxBuffSize = DataSize.ofMegabytes(20);
@BeforeAll @BeforeAll
static void init() { static void init() {
KSQL_DB.start(); KSQL_DB.start();

View file

@ -0,0 +1,54 @@
package com.provectus.kafka.ui.util;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.time.Duration;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
class GithubReleaseInfoTest {
private final MockWebServer mockWebServer = new MockWebServer();
@BeforeEach
void startMockServer() throws IOException {
mockWebServer.start();
}
@AfterEach
void stopMockServer() throws IOException {
mockWebServer.close();
}
@Test
void test() {
mockWebServer.enqueue(new MockResponse()
.addHeader("content-type: application/json")
.setBody("""
{
"published_at": "2023-03-09T16:11:31Z",
"tag_name": "v0.6.0",
"html_url": "https://github.com/provectus/kafka-ui/releases/tag/v0.6.0",
"some_unused_prop": "ololo"
}
"""));
var url = mockWebServer.url("repos/provectus/kafka-ui/releases/latest").toString();
var infoHolder = new GithubReleaseInfo(url);
infoHolder.refresh().block();
var i = infoHolder.get();
assertThat(i.html_url())
.isEqualTo("https://github.com/provectus/kafka-ui/releases/tag/v0.6.0");
assertThat(i.published_at())
.isEqualTo("2023-03-09T16:11:31Z");
assertThat(i.tag_name())
.isEqualTo("v0.6.0");
}
}

View file

@ -27,20 +27,24 @@
<artifactId>spring-boot-starter-validation</artifactId> <artifactId>spring-boot-starter-validation</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.swagger</groupId> <groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId> <artifactId>swagger-integration-jakarta</artifactId>
<version>${swagger-annotations.version}</version> <version>2.2.8</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.openapitools</groupId> <groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId> <artifactId>jackson-databind-nullable</artifactId>
<version>${jackson-databind-nullable.version}</version> <version>0.2.4</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.code.findbugs</groupId> <groupId>jakarta.annotation</groupId>
<artifactId>jsr305</artifactId> <artifactId>jakarta.annotation-api</artifactId>
<version>3.0.2</version> <version>2.1.1</version>
<scope>provided</scope> </dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency> </dependency>
</dependencies> </dependencies>
@ -71,6 +75,7 @@
<library>webclient</library> <library>webclient</library>
<useBeanValidation>true</useBeanValidation> <useBeanValidation>true</useBeanValidation>
<dateLibrary>java8</dateLibrary> <dateLibrary>java8</dateLibrary>
<useJakartaEe>true</useJakartaEe>
</configOptions> </configOptions>
</configuration> </configuration>
</execution> </execution>
@ -80,8 +85,7 @@
<goal>generate</goal> <goal>generate</goal>
</goals> </goals>
<configuration> <configuration>
<inputSpec>${project.basedir}/src/main/resources/swagger/kafka-ui-api.yaml <inputSpec>${project.basedir}/src/main/resources/swagger/kafka-ui-api.yaml</inputSpec>
</inputSpec>
<output>${project.build.directory}/generated-sources/api</output> <output>${project.build.directory}/generated-sources/api</output>
<generatorName>spring</generatorName> <generatorName>spring</generatorName>
<modelNameSuffix>DTO</modelNameSuffix> <modelNameSuffix>DTO</modelNameSuffix>
@ -89,14 +93,12 @@
<modelPackage>com.provectus.kafka.ui.model</modelPackage> <modelPackage>com.provectus.kafka.ui.model</modelPackage>
<apiPackage>com.provectus.kafka.ui.api</apiPackage> <apiPackage>com.provectus.kafka.ui.api</apiPackage>
<sourceFolder>kafka-ui-contract</sourceFolder> <sourceFolder>kafka-ui-contract</sourceFolder>
<reactive>true</reactive> <reactive>true</reactive>
<interfaceOnly>true</interfaceOnly> <interfaceOnly>true</interfaceOnly>
<skipDefaultInterface>true</skipDefaultInterface> <skipDefaultInterface>true</skipDefaultInterface>
<useBeanValidation>true</useBeanValidation> <useBeanValidation>true</useBeanValidation>
<useTags>true</useTags> <useTags>true</useTags>
<useSpringBoot3>true</useSpringBoot3>
<dateLibrary>java8</dateLibrary> <dateLibrary>java8</dateLibrary>
</configOptions> </configOptions>
<typeMappings> <typeMappings>
@ -116,15 +118,13 @@
<generatorName>java</generatorName> <generatorName>java</generatorName>
<generateApiTests>false</generateApiTests> <generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests> <generateModelTests>false</generateModelTests>
<configOptions> <configOptions>
<modelPackage>com.provectus.kafka.ui.connect.model</modelPackage> <modelPackage>com.provectus.kafka.ui.connect.model</modelPackage>
<apiPackage>com.provectus.kafka.ui.connect.api</apiPackage> <apiPackage>com.provectus.kafka.ui.connect.api</apiPackage>
<sourceFolder>kafka-connect-client</sourceFolder> <sourceFolder>kafka-connect-client</sourceFolder>
<asyncNative>true</asyncNative> <asyncNative>true</asyncNative>
<library>webclient</library> <library>webclient</library>
<useJakartaEe>true</useJakartaEe>
<useBeanValidation>true</useBeanValidation> <useBeanValidation>true</useBeanValidation>
<dateLibrary>java8</dateLibrary> <dateLibrary>java8</dateLibrary>
</configOptions> </configOptions>
@ -142,15 +142,13 @@
<generatorName>java</generatorName> <generatorName>java</generatorName>
<generateApiTests>false</generateApiTests> <generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests> <generateModelTests>false</generateModelTests>
<configOptions> <configOptions>
<modelPackage>com.provectus.kafka.ui.sr.model</modelPackage> <modelPackage>com.provectus.kafka.ui.sr.model</modelPackage>
<apiPackage>com.provectus.kafka.ui.sr.api</apiPackage> <apiPackage>com.provectus.kafka.ui.sr.api</apiPackage>
<sourceFolder>kafka-sr-client</sourceFolder> <sourceFolder>kafka-sr-client</sourceFolder>
<asyncNative>true</asyncNative> <asyncNative>true</asyncNative>
<library>webclient</library> <library>webclient</library>
<useJakartaEe>true</useJakartaEe>
<useBeanValidation>true</useBeanValidation> <useBeanValidation>true</useBeanValidation>
<dateLibrary>java8</dateLibrary> <dateLibrary>java8</dateLibrary>
</configOptions> </configOptions>

View file

@ -2021,6 +2021,26 @@ components:
type: string type: string
enum: enum:
- DYNAMIC_CONFIG - DYNAMIC_CONFIG
build:
type: object
properties:
commitId:
type: string
version:
type: string
buildTime:
type: string
isLatestRelease:
type: boolean
latestRelease:
type: object
properties:
versionTag:
type: string
publishedAt:
type: string
htmlUrl:
type: string
Cluster: Cluster:
type: object type: object
@ -2493,6 +2513,10 @@ components:
- UNKNOWN - UNKNOWN
ConsumerGroup: ConsumerGroup:
discriminator:
propertyName: inherit
mapping:
details: "#/components/schemas/ConsumerGroupDetails"
type: object type: object
properties: properties:
groupId: groupId:

View file

@ -27,7 +27,7 @@ This repository is for E2E UI automation.
``` ```
git clone https://github.com/provectus/kafka-ui.git git clone https://github.com/provectus/kafka-ui.git
cd kafka-ui-e2e-checks cd kafka-ui-e2e-checks
docker pull selenoid/vnc:chrome_86.0 docker pull selenoid/vnc_chrome:103.0
``` ```
### How to run checks ### How to run checks
@ -36,6 +36,7 @@ docker pull selenoid/vnc:chrome_86.0
``` ```
cd kafka-ui cd kafka-ui
docker-compose -f kafka-ui-e2e-checks/docker/selenoid-local.yaml up -d
docker-compose -f documentation/compose/e2e-tests.yaml up -d docker-compose -f documentation/compose/e2e-tests.yaml up -d
``` ```
@ -51,6 +52,14 @@ docker-compose -f documentation/compose/e2e-tests.yaml up -d
-Dbrowser=local -Dbrowser=local
``` ```
Expected Location of Chrome
```
Linux: /usr/bin/google-chrome1
Mac: /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome
Windows XP: %HOMEPATH%\Local Settings\Application Data\Google\Chrome\Application\chrome.exe
Windows Vista and newer: C:\Users%USERNAME%\AppData\Local\Google\Chrome\Application\chrome.exe
```
### Qase integration ### Qase integration
Found instruction for Qase.io integration (for internal use only) at `kafka-ui-e2e-checks/QASE.md` Found instruction for Qase.io integration (for internal use only) at `kafka-ui-e2e-checks/QASE.md`

View file

@ -1,17 +1,19 @@
---
version: '3' version: '3'
services: services:
selenoid: selenoid:
network_mode: bridge network_mode: bridge
image: aerokube/selenoid:1.10.7 image: aerokube/selenoid:1.10.7
volumes: volumes:
- "../selenoid/config:/etc/selenoid" - "../selenoid/config:/etc/selenoid"
- "/var/run/docker.sock:/var/run/docker.sock" - "/var/run/docker.sock:/var/run/docker.sock"
- "../selenoid/video:/video" - "../selenoid/video:/opt/selenoid/video"
- "../selenoid/logs:/opt/selenoid/logs" - "../selenoid/logs:/opt/selenoid/logs"
environment: environment:
- OVERRIDE_VIDEO_OUTPUT_DIR=video - OVERRIDE_VIDEO_OUTPUT_DIR=../selenoid/video
command: [ "-conf", "/etc/selenoid/browsers.json", "-video-output-dir", "/opt/selenoid/video", "-log-output-dir", "/opt/selenoid/logs" ] command: [ "-conf", "/etc/selenoid/browsersGit.json", "-video-output-dir", "/opt/selenoid/video", "-log-output-dir", "/opt/selenoid/logs" ]
ports: ports:
- "4444:4444" - "4444:4444"
@ -22,10 +24,10 @@ services:
- selenoid - selenoid
ports: ports:
- "8081:8080" - "8081:8080"
command: [ "--selenoid-uri", "http://localhost:4444" ] command: [ "--selenoid-uri", "http://selenoid:4444" ]
selenoid-chrome: selenoid-chrome:
network_mode: bridge network_mode: bridge
image: selenoid/vnc:chrome_96.0 image: selenoid/vnc_chrome:103.0
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"

View file

@ -0,0 +1,33 @@
---
version: '3'
services:
selenoid:
network_mode: bridge
image: aerokube/selenoid:1.10.7
volumes:
- "../selenoid/config:/etc/selenoid"
- "/var/run/docker.sock:/var/run/docker.sock"
- "../selenoid/video:/opt/selenoid/video"
- "../selenoid/logs:/opt/selenoid/logs"
environment:
- OVERRIDE_VIDEO_OUTPUT_DIR=../selenoid/video
command: [ "-conf", "/etc/selenoid/browsersLocal.json", "-video-output-dir", "/opt/selenoid/video", "-log-output-dir", "/opt/selenoid/logs" ]
ports:
- "4444:4444"
selenoid-ui:
network_mode: bridge
image: aerokube/selenoid-ui:latest-release
links:
- selenoid
ports:
- "8081:8080"
command: [ "--selenoid-uri", "http://selenoid:4444" ]
selenoid-chrome:
network_mode: bridge
image: selenoid/vnc_chrome:103.0
extra_hosts:
- "host.docker.internal:host-gateway"

View file

@ -17,15 +17,14 @@
<testcontainers.version>1.17.6</testcontainers.version> <testcontainers.version>1.17.6</testcontainers.version>
<httpcomponents.version>5.2.1</httpcomponents.version> <httpcomponents.version>5.2.1</httpcomponents.version>
<selenium.version>4.8.1</selenium.version> <selenium.version>4.8.1</selenium.version>
<selenide.version>6.11.2</selenide.version> <selenide.version>6.12.3</selenide.version>
<testng.version>7.7.0</testng.version> <testng.version>7.7.0</testng.version>
<allure.version>2.21.0</allure.version> <allure.version>2.21.0</allure.version>
<qase.io.version>3.0.3</qase.io.version> <qase.io.version>3.0.4</qase.io.version>
<aspectj.version>1.9.9.1</aspectj.version> <aspectj.version>1.9.9.1</aspectj.version>
<assertj.version>3.24.2</assertj.version> <assertj.version>3.24.2</assertj.version>
<hamcrest.version>2.2</hamcrest.version> <hamcrest.version>2.2</hamcrest.version>
<slf4j.version>1.7.36</slf4j.version> <slf4j.version>2.0.5</slf4j.version>
<dotenv.version>2.3.1</dotenv.version>
<kafka.version>3.3.1</kafka.version> <kafka.version>3.3.1</kafka.version>
</properties> </properties>
@ -122,6 +121,11 @@
<artifactId>selenium</artifactId> <artifactId>selenium</artifactId>
<version>${testcontainers.version}</version> <version>${testcontainers.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.httpcomponents.core5</groupId> <groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId> <artifactId>httpcore5</artifactId>
@ -132,6 +136,11 @@
<artifactId>httpclient5</artifactId> <artifactId>httpclient5</artifactId>
<version>${httpcomponents.version}</version> <version>${httpcomponents.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-http-jdk-client</artifactId>
<version>${selenium.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.seleniumhq.selenium</groupId> <groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-http</artifactId> <artifactId>selenium-http</artifactId>
@ -187,16 +196,6 @@
<artifactId>slf4j-simple</artifactId> <artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version> <version>${slf4j.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</dependency>
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>dotenv-java</artifactId>
<version>${dotenv.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.provectus</groupId> <groupId>com.provectus</groupId>
<artifactId>kafka-ui-contract</artifactId> <artifactId>kafka-ui-contract</artifactId>
@ -265,6 +264,37 @@
<artifactId>allure-maven</artifactId> <artifactId>allure-maven</artifactId>
<version>2.10.0</version> <version>2.10.0</version>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.1.2</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>10.3.1</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>checkstyle</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<violationSeverity>warning</violationSeverity>
<failOnViolation>true</failOnViolation>
<failsOnError>true</failsOnError>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
<configLocation>file:${basedir}/../etc/checkstyle/checkstyle-e2e.xml</configLocation>
<headerLocation>file:${basedir}/../etc/checkstyle/apache-header.txt</headerLocation>
</configuration>
</execution>
</executions>
</plugin>
</plugins> </plugins>
</build> </build>
</profile> </profile>

View file

@ -0,0 +1,15 @@
{
"chrome": {
"default": "103.0",
"versions": {
"103.0": {
"image": "selenoid/vnc_chrome:103.0",
"hosts": [
"host.docker.internal:172.17.0.1"
],
"port": "4444",
"path": "/"
}
}
}
}

View file

@ -1,9 +1,9 @@
{ {
"chrome": { "chrome": {
"default": "96.0", "default": "103.0",
"versions": { "versions": {
"96.0": { "103.0": {
"image": "selenoid/vnc_chrome:96.0", "image": "selenoid/vnc_chrome:103.0",
"port": "4444", "port": "4444",
"path": "/" "path": "/"
} }

View file

@ -7,5 +7,5 @@ import lombok.experimental.Accessors;
@Accessors(chain = true) @Accessors(chain = true)
public class Connector { public class Connector {
private String name, config; private String name, config;
} }

View file

@ -1,33 +1,34 @@
package com.provectus.kafka.ui.models; package com.provectus.kafka.ui.models;
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
import com.provectus.kafka.ui.api.model.SchemaType; import com.provectus.kafka.ui.api.model.SchemaType;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
public class Schema { public class Schema {
private String name, valuePath; private String name, valuePath;
private SchemaType type; private SchemaType type;
public static Schema createSchemaAvro() { public static Schema createSchemaAvro() {
return new Schema().setName("schema_avro-" + randomAlphabetic(5)) return new Schema().setName("schema_avro-" + randomAlphabetic(5))
.setType(SchemaType.AVRO) .setType(SchemaType.AVRO)
.setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schemas/schema_avro_value.json"); .setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schemas/schema_avro_value.json");
} }
public static Schema createSchemaJson() { public static Schema createSchemaJson() {
return new Schema().setName("schema_json-" + randomAlphabetic(5)) return new Schema().setName("schema_json-" + randomAlphabetic(5))
.setType(SchemaType.JSON) .setType(SchemaType.JSON)
.setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schemas/schema_json_Value.json"); .setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schemas/schema_json_Value.json");
} }
public static Schema createSchemaProtobuf() { public static Schema createSchemaProtobuf() {
return new Schema().setName("schema_protobuf-" + randomAlphabetic(5)) return new Schema().setName("schema_protobuf-" + randomAlphabetic(5))
.setType(SchemaType.PROTOBUF) .setType(SchemaType.PROTOBUF)
.setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schemas/schema_protobuf_value.txt"); .setValuePath(
} System.getProperty("user.dir") + "/src/main/resources/testData/schemas/schema_protobuf_value.txt");
}
} }

View file

@ -11,10 +11,10 @@ import lombok.experimental.Accessors;
@Accessors(chain = true) @Accessors(chain = true)
public class Topic { public class Topic {
private String name, timeToRetainData, maxMessageBytes, messageKey, messageContent, customParameterValue; private String name, timeToRetainData, maxMessageBytes, messageKey, messageValue, customParameterValue;
private int numberOfPartitions; private int numberOfPartitions;
private CustomParameterType customParameterType; private CustomParameterType customParameterType;
private CleanupPolicyValue cleanupPolicyValue; private CleanupPolicyValue cleanupPolicyValue;
private MaxSizeOnDisk maxSizeOnDisk; private MaxSizeOnDisk maxSizeOnDisk;
private TimeToRetain timeToRetain; private TimeToRetain timeToRetain;
} }

View file

@ -1,135 +1,142 @@
package com.provectus.kafka.ui.pages; 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.Condition;
import com.codeborne.selenide.ElementsCollection; import com.codeborne.selenide.ElementsCollection;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.codeborne.selenide.WebDriverRunner; import com.codeborne.selenide.WebDriverRunner;
import com.provectus.kafka.ui.pages.panels.enums.MenuItem; import com.provectus.kafka.ui.pages.panels.enums.MenuItem;
import com.provectus.kafka.ui.utilities.WebUtils; import com.provectus.kafka.ui.utilities.WebUtils;
import java.time.Duration;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.Keys; import org.openqa.selenium.Keys;
import org.openqa.selenium.interactions.Actions; import org.openqa.selenium.interactions.Actions;
import java.time.Duration;
import static com.codeborne.selenide.Selenide.$$x;
import static com.codeborne.selenide.Selenide.$x;
@Slf4j @Slf4j
public abstract class BasePage extends WebUtils { public abstract class BasePage extends WebUtils {
protected SelenideElement loadingSpinner = $x("//div[@role='progressbar']"); protected SelenideElement loadingSpinner = $x("//div[@role='progressbar']");
protected SelenideElement submitBtn = $x("//button[@type='submit']"); protected SelenideElement submitBtn = $x("//button[@type='submit']");
protected SelenideElement tableGrid = $x("//table"); protected SelenideElement tableGrid = $x("//table");
protected SelenideElement dotMenuBtn = $x("//button[@aria-label='Dropdown Toggle']"); protected SelenideElement searchFld = $x("//input[@type='text'][contains(@id, ':r')]");
protected SelenideElement alertHeader = $x("//div[@role='alert']//div[@role='heading']"); protected SelenideElement dotMenuBtn = $x("//button[@aria-label='Dropdown Toggle']");
protected SelenideElement alertMessage = $x("//div[@role='alert']//div[@role='contentinfo']"); protected SelenideElement alertHeader = $x("//div[@role='alert']//div[@role='heading']");
protected SelenideElement confirmationMdl = $x("//div[text()= 'Confirm the action']/.."); protected SelenideElement alertMessage = $x("//div[@role='alert']//div[@role='contentinfo']");
protected SelenideElement confirmBtn = $x("//button[contains(text(),'Confirm')]"); protected SelenideElement confirmationMdl = $x("//div[text()= 'Confirm the action']/..");
protected SelenideElement cancelBtn = $x("//button[contains(text(),'Cancel')]"); protected SelenideElement confirmBtn = $x("//button[contains(text(),'Confirm')]");
protected SelenideElement backBtn = $x("//button[contains(text(),'Back')]"); protected SelenideElement cancelBtn = $x("//button[contains(text(),'Cancel')]");
protected SelenideElement nextBtn = $x("//button[contains(text(),'Next')]"); protected SelenideElement backBtn = $x("//button[contains(text(),'Back')]");
protected ElementsCollection ddlOptions = $$x("//li[@value]"); protected SelenideElement nextBtn = $x("//button[contains(text(),'Next')]");
protected ElementsCollection gridItems = $$x("//tr[@class]"); protected ElementsCollection ddlOptions = $$x("//li[@value]");
protected String summaryCellLocator = "//div[contains(text(),'%s')]"; protected ElementsCollection gridItems = $$x("//tr[@class]");
protected String tableElementNameLocator = "//tbody//a[contains(text(),'%s')]"; protected String summaryCellLocator = "//div[contains(text(),'%s')]";
protected String columnHeaderLocator = "//table//tr/th//div[text()='%s']"; protected String tableElementNameLocator = "//tbody//a[contains(text(),'%s')]";
protected String pageTitleFromHeader = "//h1[text()='%s']"; protected String columnHeaderLocator = "//table//tr/th//div[text()='%s']";
protected String pagePathFromHeader = "//a[text()='%s']/../h1"; protected String pageTitleFromHeader = "//h1[text()='%s']";
protected String pagePathFromHeader = "//a[text()='%s']/../h1";
protected void waitUntilSpinnerDisappear() { protected void waitUntilSpinnerDisappear(int... timeoutInSeconds) {
log.debug("\nwaitUntilSpinnerDisappear"); log.debug("\nwaitUntilSpinnerDisappear");
if (isVisible(loadingSpinner)) { if (isVisible(loadingSpinner, timeoutInSeconds)) {
loadingSpinner.shouldBe(Condition.disappear, Duration.ofSeconds(60)); loadingSpinner.shouldBe(Condition.disappear, Duration.ofSeconds(60));
} }
}
protected void searchItem(String tag) {
log.debug("\nsearchItem: {}", tag);
sendKeysAfterClear(searchFld, tag);
searchFld.pressEnter().shouldHave(Condition.value(tag));
waitUntilSpinnerDisappear(1);
}
protected SelenideElement getPageTitleFromHeader(MenuItem menuItem) {
return $x(String.format(pageTitleFromHeader, menuItem.getPageTitle()));
}
protected SelenideElement getPagePathFromHeader(MenuItem menuItem) {
return $x(String.format(pagePathFromHeader, menuItem.getPageTitle()));
}
protected void clickSubmitBtn() {
clickByJavaScript(submitBtn);
}
protected void setJsonInputValue(SelenideElement jsonInput, String jsonConfig) {
sendKeysByActions(jsonInput, jsonConfig.replace(" ", ""));
new Actions(WebDriverRunner.getWebDriver())
.keyDown(Keys.SHIFT)
.sendKeys(Keys.PAGE_DOWN)
.keyUp(Keys.SHIFT)
.sendKeys(Keys.DELETE)
.perform();
}
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;
} }
protected SelenideElement getPageTitleFromHeader(MenuItem menuItem) { public String toString() {
return $x(String.format(pageTitleFromHeader, menuItem.getPageTitle())); return value;
}
protected SelenideElement getPagePathFromHeader(MenuItem menuItem) {
return $x(String.format(pagePathFromHeader, menuItem.getPageTitle()));
}
protected void clickSubmitBtn() {
clickByJavaScript(submitBtn);
}
protected void setJsonInputValue(SelenideElement jsonInput, String jsonConfig) {
sendKeysByActions(jsonInput, jsonConfig.replace(" ", ""));
new Actions(WebDriverRunner.getWebDriver())
.keyDown(Keys.SHIFT)
.sendKeys(Keys.PAGE_DOWN)
.keyUp(Keys.SHIFT)
.sendKeys(Keys.DELETE)
.perform();
}
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;
}
} }
}
} }

View file

@ -1,41 +1,40 @@
package com.provectus.kafka.ui.pages.brokers; 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.Condition;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static com.codeborne.selenide.Selenide.$$x;
import static com.codeborne.selenide.Selenide.$x;
public class BrokersConfigTab extends BasePage { public class BrokersConfigTab extends BasePage {
protected List<SelenideElement> editBtn = $$x("//button[@aria-label='editAction']"); protected List<SelenideElement> editBtn = $$x("//button[@aria-label='editAction']");
protected SelenideElement searchByKeyField = $x("//input[@placeholder='Search by Key']"); protected SelenideElement searchByKeyField = $x("//input[@placeholder='Search by Key']");
@Step @Step
public BrokersConfigTab waitUntilScreenReady() { public BrokersConfigTab waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
searchByKeyField.shouldBe(Condition.visible); searchByKeyField.shouldBe(Condition.visible);
return this; return this;
} }
@Step @Step
public boolean isSearchByKeyVisible() { public boolean isSearchByKeyVisible() {
return isVisible(searchByKeyField); return isVisible(searchByKeyField);
} }
public List<SelenideElement> getColumnHeaders() { public List<SelenideElement> getColumnHeaders() {
return Stream.of("Key", "Value", "Source") return Stream.of("Key", "Value", "Source")
.map(name -> $x(String.format(columnHeaderLocator, name))) .map(name -> $x(String.format(columnHeaderLocator, name)))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public List<SelenideElement> getEditButtons() { public List<SelenideElement> getEditButtons() {
return editBtn; return editBtn;
} }
} }

View file

@ -1,92 +1,91 @@
package com.provectus.kafka.ui.pages.brokers; 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.Condition;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import org.openqa.selenium.By;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; 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 { public class BrokersDetails extends BasePage {
protected SelenideElement logDirectoriesTab = $x("//a[text()='Log directories']"); protected SelenideElement logDirectoriesTab = $x("//a[text()='Log directories']");
protected SelenideElement metricsTab = $x("//a[text()='Metrics']"); protected SelenideElement metricsTab = $x("//a[text()='Metrics']");
protected String brokersTabLocator = "//a[text()='%s']"; protected String brokersTabLocator = "//a[text()='%s']";
@Step @Step
public BrokersDetails waitUntilScreenReady() { public BrokersDetails waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
Arrays.asList(logDirectoriesTab, metricsTab).forEach(element -> element.shouldBe(Condition.visible)); Arrays.asList(logDirectoriesTab, metricsTab).forEach(element -> element.shouldBe(Condition.visible));
return this; return this;
}
@Step
public BrokersDetails openDetailsTab(DetailsTab menu) {
$(By.linkText(menu.toString())).shouldBe(Condition.enabled).click();
waitUntilSpinnerDisappear();
return this;
}
private List<SelenideElement> getVisibleColumnHeaders() {
return Stream.of("Name", "Topics", "Error", "Partitions")
.map(name -> $x(String.format(columnHeaderLocator, name)))
.collect(Collectors.toList());
}
private List<SelenideElement> getEnabledColumnHeaders() {
return Stream.of("Name", "Error")
.map(name -> $x(String.format(columnHeaderLocator, name)))
.collect(Collectors.toList());
}
private List<SelenideElement> getVisibleSummaryCells() {
return Stream.of("Segment Size", "Segment Count", "Port", "Host")
.map(name -> $x(String.format(summaryCellLocator, name)))
.collect(Collectors.toList());
}
private List<SelenideElement> getDetailsTabs() {
return Stream.of(DetailsTab.values())
.map(name -> $x(String.format(brokersTabLocator, name)))
.collect(Collectors.toList());
}
@Step
public List<SelenideElement> getAllEnabledElements() {
List<SelenideElement> enabledElements = new ArrayList<>(getEnabledColumnHeaders());
enabledElements.addAll(getDetailsTabs());
return enabledElements;
}
@Step
public List<SelenideElement> getAllVisibleElements() {
List<SelenideElement> 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 String toString() {
public BrokersDetails openDetailsTab(DetailsTab menu) { return value;
$(By.linkText(menu.toString())).shouldBe(Condition.enabled).click();
waitUntilSpinnerDisappear();
return this;
}
private List<SelenideElement> getVisibleColumnHeaders() {
return Stream.of("Name", "Topics", "Error", "Partitions")
.map(name -> $x(String.format(columnHeaderLocator, name)))
.collect(Collectors.toList());
}
private List<SelenideElement> getEnabledColumnHeaders() {
return Stream.of("Name", "Error")
.map(name -> $x(String.format(columnHeaderLocator, name)))
.collect(Collectors.toList());
}
private List<SelenideElement> getVisibleSummaryCells() {
return Stream.of("Segment Size", "Segment Count", "Port", "Host")
.map(name -> $x(String.format(summaryCellLocator, name)))
.collect(Collectors.toList());
}
private List<SelenideElement> getDetailsTabs() {
return Stream.of(DetailsTab.values())
.map(name -> $x(String.format(brokersTabLocator, name)))
.collect(Collectors.toList());
}
@Step
public List<SelenideElement> getAllEnabledElements() {
List<SelenideElement> enabledElements = new ArrayList<>(getEnabledColumnHeaders());
enabledElements.addAll(getDetailsTabs());
return enabledElements;
}
@Step
public List<SelenideElement> getAllVisibleElements() {
List<SelenideElement> 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;
}
} }
}
} }

View file

@ -1,123 +1,122 @@
package com.provectus.kafka.ui.pages.brokers; package com.provectus.kafka.ui.pages.brokers;
import static com.codeborne.selenide.Selenide.$x;
import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.BROKERS;
import com.codeborne.selenide.CollectionCondition; import com.codeborne.selenide.CollectionCondition;
import com.codeborne.selenide.Condition; import com.codeborne.selenide.Condition;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static com.codeborne.selenide.Selenide.$x;
import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.BROKERS;
public class BrokersList extends BasePage { public class BrokersList extends BasePage {
@Step @Step
public BrokersList waitUntilScreenReady() { public BrokersList waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
getPageTitleFromHeader(BROKERS).shouldBe(Condition.visible); getPageTitleFromHeader(BROKERS).shouldBe(Condition.visible);
return this; return this;
}
@Step
public BrokersList openBroker(int brokerId) {
getBrokerItem(brokerId).openItem();
return this;
}
private List<SelenideElement> getUptimeSummaryCells() {
return Stream.of("Broker Count", "Active Controller", "Version")
.map(name -> $x(String.format(summaryCellLocator, name)))
.collect(Collectors.toList());
}
private List<SelenideElement> 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<SelenideElement> getAllVisibleElements() {
List<SelenideElement> visibleElements = new ArrayList<>(getUptimeSummaryCells());
visibleElements.addAll(getPartitionsSummaryCells());
return visibleElements;
}
private List<SelenideElement> 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<SelenideElement> getAllEnabledElements() {
return getEnabledColumnHeaders();
}
private List<BrokersList.BrokerGridItem> initGridItems() {
List<BrokersList.BrokerGridItem> gridItemList = new ArrayList<>();
gridItems.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().orElseThrow();
}
@Step
public List<BrokerGridItem> 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 @Step
public BrokersList openBroker(int brokerId) { public int getId() {
getBrokerItem(brokerId).openItem(); return Integer.parseInt(getIdElm().getText().trim());
return this;
}
private List<SelenideElement> getUptimeSummaryCells() {
return Stream.of("Broker Count", "Active Controller", "Version")
.map(name -> $x(String.format(summaryCellLocator, name)))
.collect(Collectors.toList());
}
private List<SelenideElement> getPartitionsSummaryCells() {
return Stream.of("Online", "URP", "In Sync Replicas", "Out Of Sync Replicas")
.map(name -> $x(String.format(summaryCellLocator, name)))
.collect(Collectors.toList());
} }
@Step @Step
public List<SelenideElement> getAllVisibleElements() { public void openItem() {
List<SelenideElement> visibleElements = new ArrayList<>(getUptimeSummaryCells()); getIdElm().click();
visibleElements.addAll(getPartitionsSummaryCells());
return visibleElements;
}
private List<SelenideElement> getEnabledColumnHeaders() {
return Stream.of("Broker ID", "Segment Size", "Segment Count", "Port", "Host")
.map(name -> $x(String.format(columnHeaderLocator, name)))
.collect(Collectors.toList());
} }
@Step @Step
public List<SelenideElement> getAllEnabledElements() { public int getSegmentSize() {
return getEnabledColumnHeaders(); return Integer.parseInt(element.$x("./td[2]").getText().trim());
}
private List<BrokersList.BrokerGridItem> initGridItems() {
List<BrokersList.BrokerGridItem> gridItemList = new ArrayList<>();
gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))
.forEach(item -> gridItemList.add(new BrokersList.BrokerGridItem(item)));
return gridItemList;
} }
@Step @Step
public BrokerGridItem getBrokerItem(int id) { public int getSegmentCount() {
return initGridItems().stream() return Integer.parseInt(element.$x("./td[3]").getText().trim());
.filter(e -> e.getId() == id)
.findFirst().orElseThrow();
} }
@Step @Step
public List<BrokerGridItem> getAllBrokers() { public int getPort() {
return initGridItems(); return Integer.parseInt(element.$x("./td[4]").getText().trim());
} }
public static class BrokerGridItem extends BasePage { @Step
public String getHost() {
private final SelenideElement element; return element.$x("./td[5]").getText().trim();
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();
}
} }
}
} }

View file

@ -1,49 +1,49 @@
package com.provectus.kafka.ui.pages.connectors; package com.provectus.kafka.ui.pages.connectors;
import static com.codeborne.selenide.Selenide.$x;
import com.codeborne.selenide.Condition; import com.codeborne.selenide.Condition;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import static com.codeborne.selenide.Selenide.$x;
public class ConnectorCreateForm extends BasePage { public class ConnectorCreateForm extends BasePage {
protected SelenideElement nameField = $x("//input[@name='name']"); protected SelenideElement nameField = $x("//input[@name='name']");
protected SelenideElement contentTextArea = $x("//textarea[@class='ace_text-input']"); protected SelenideElement contentTextArea = $x("//textarea[@class='ace_text-input']");
protected SelenideElement configField = $x("//div[@id='config']"); protected SelenideElement configField = $x("//div[@id='config']");
@Step @Step
public ConnectorCreateForm waitUntilScreenReady() { public ConnectorCreateForm waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
nameField.shouldBe(Condition.visible); nameField.shouldBe(Condition.visible);
return this; return this;
} }
@Step @Step
public ConnectorCreateForm setName(String connectName) { public ConnectorCreateForm setName(String connectName) {
nameField.shouldBe(Condition.enabled).setValue(connectName); nameField.shouldBe(Condition.enabled).setValue(connectName);
return this; return this;
} }
@Step @Step
public ConnectorCreateForm setConfig(String configJson) { public ConnectorCreateForm setConfig(String configJson) {
configField.shouldBe(Condition.enabled).click(); configField.shouldBe(Condition.enabled).click();
setJsonInputValue(contentTextArea, configJson); setJsonInputValue(contentTextArea, configJson);
return this; return this;
} }
@Step @Step
public ConnectorCreateForm setConnectorDetails(String connectName, String configJson) { public ConnectorCreateForm setConnectorDetails(String connectName, String configJson) {
setName(connectName); setName(connectName);
setConfig(configJson); setConfig(configJson);
return this; return this;
} }
@Step @Step
public ConnectorCreateForm clickSubmitButton() { public ConnectorCreateForm clickSubmitButton() {
clickSubmitBtn(); clickSubmitBtn();
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
return this; return this;
} }
} }

View file

@ -1,84 +1,84 @@
package com.provectus.kafka.ui.pages.connectors; package com.provectus.kafka.ui.pages.connectors;
import static com.codeborne.selenide.Selenide.$x;
import com.codeborne.selenide.Condition; import com.codeborne.selenide.Condition;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import static com.codeborne.selenide.Selenide.$x;
public class ConnectorDetails extends BasePage { public class ConnectorDetails extends BasePage {
protected SelenideElement deleteBtn = $x("//li/div[contains(text(),'Delete')]"); protected SelenideElement deleteBtn = $x("//li/div[contains(text(),'Delete')]");
protected SelenideElement confirmBtnMdl = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]"); protected SelenideElement confirmBtnMdl = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]");
protected SelenideElement contentTextArea = $x("//textarea[@class='ace_text-input']"); protected SelenideElement contentTextArea = $x("//textarea[@class='ace_text-input']");
protected SelenideElement taskTab = $x("//a[contains(text(),'Tasks')]"); protected SelenideElement taskTab = $x("//a[contains(text(),'Tasks')]");
protected SelenideElement configTab = $x("//a[contains(text(),'Config')]"); protected SelenideElement configTab = $x("//a[contains(text(),'Config')]");
protected SelenideElement configField = $x("//div[@id='config']"); protected SelenideElement configField = $x("//div[@id='config']");
protected String connectorHeaderLocator = "//h1[contains(text(),'%s')]"; protected String connectorHeaderLocator = "//h1[contains(text(),'%s')]";
@Step @Step
public ConnectorDetails waitUntilScreenReady() { public ConnectorDetails waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
dotMenuBtn.shouldBe(Condition.visible); dotMenuBtn.shouldBe(Condition.visible);
return this; return this;
} }
@Step @Step
public ConnectorDetails openConfigTab() { public ConnectorDetails openConfigTab() {
clickByJavaScript(configTab); clickByJavaScript(configTab);
return this; return this;
} }
@Step @Step
public ConnectorDetails setConfig(String configJson) { public ConnectorDetails setConfig(String configJson) {
configField.shouldBe(Condition.enabled).click(); configField.shouldBe(Condition.enabled).click();
clearByKeyboard(contentTextArea); clearByKeyboard(contentTextArea);
contentTextArea.setValue(configJson); contentTextArea.setValue(configJson);
configField.shouldBe(Condition.enabled).click(); configField.shouldBe(Condition.enabled).click();
return this; return this;
} }
@Step @Step
public ConnectorDetails clickSubmitButton() { public ConnectorDetails clickSubmitButton() {
clickSubmitBtn(); clickSubmitBtn();
return this; return this;
} }
@Step @Step
public ConnectorDetails openDotMenu() { public ConnectorDetails openDotMenu() {
clickByJavaScript(dotMenuBtn); clickByJavaScript(dotMenuBtn);
return this; return this;
} }
@Step @Step
public ConnectorDetails clickDeleteBtn() { public ConnectorDetails clickDeleteBtn() {
clickByJavaScript(deleteBtn); clickByJavaScript(deleteBtn);
return this; return this;
} }
@Step @Step
public ConnectorDetails clickConfirmBtn() { public ConnectorDetails clickConfirmBtn() {
confirmBtnMdl.shouldBe(Condition.enabled).click(); confirmBtnMdl.shouldBe(Condition.enabled).click();
confirmBtnMdl.shouldBe(Condition.disappear); confirmBtnMdl.shouldBe(Condition.disappear);
return this; return this;
} }
@Step @Step
public ConnectorDetails deleteConnector() { public ConnectorDetails deleteConnector() {
openDotMenu(); openDotMenu();
clickDeleteBtn(); clickDeleteBtn();
clickConfirmBtn(); clickConfirmBtn();
return this; return this;
} }
@Step @Step
public boolean isConnectorHeaderVisible(String connectorName) { public boolean isConnectorHeaderVisible(String connectorName) {
return isVisible($x(String.format(connectorHeaderLocator, connectorName))); return isVisible($x(String.format(connectorHeaderLocator, connectorName)));
} }
@Step @Step
public boolean isAlertWithMessageVisible(AlertHeader header, String message) { public boolean isAlertWithMessageVisible(AlertHeader header, String message) {
return isAlertVisible(header, message); return isAlertVisible(header, message);
} }
} }

View file

@ -1,44 +1,44 @@
package com.provectus.kafka.ui.pages.connectors; package com.provectus.kafka.ui.pages.connectors;
import static com.codeborne.selenide.Selenide.$x;
import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KAFKA_CONNECT;
import com.codeborne.selenide.Condition; import com.codeborne.selenide.Condition;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import static com.codeborne.selenide.Selenide.$x;
import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KAFKA_CONNECT;
public class KafkaConnectList extends BasePage { public class KafkaConnectList extends BasePage {
protected SelenideElement createConnectorBtn = $x("//button[contains(text(),'Create Connector')]"); protected SelenideElement createConnectorBtn = $x("//button[contains(text(),'Create Connector')]");
public KafkaConnectList() { public KafkaConnectList() {
tableElementNameLocator = "//tbody//td[contains(text(),'%s')]"; tableElementNameLocator = "//tbody//td[contains(text(),'%s')]";
} }
@Step @Step
public KafkaConnectList waitUntilScreenReady() { public KafkaConnectList waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
getPageTitleFromHeader(KAFKA_CONNECT).shouldBe(Condition.visible); getPageTitleFromHeader(KAFKA_CONNECT).shouldBe(Condition.visible);
return this; return this;
} }
@Step @Step
public KafkaConnectList clickCreateConnectorBtn() { public KafkaConnectList clickCreateConnectorBtn() {
clickByJavaScript(createConnectorBtn); clickByJavaScript(createConnectorBtn);
return this; return this;
} }
@Step @Step
public KafkaConnectList openConnector(String connectorName) { public KafkaConnectList openConnector(String connectorName) {
getTableElement(connectorName).shouldBe(Condition.enabled).click(); getTableElement(connectorName).shouldBe(Condition.enabled).click();
return this; return this;
} }
@Step @Step
public boolean isConnectorVisible(String connectorName) { public boolean isConnectorVisible(String connectorName) {
tableGrid.shouldBe(Condition.visible); tableGrid.shouldBe(Condition.visible);
return isVisible(getTableElement(connectorName)); return isVisible(getTableElement(connectorName));
} }
} }

View file

@ -1,31 +1,31 @@
package com.provectus.kafka.ui.pages.consumers; package com.provectus.kafka.ui.pages.consumers;
import static com.codeborne.selenide.Selenide.$x;
import com.codeborne.selenide.Condition; import com.codeborne.selenide.Condition;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import static com.codeborne.selenide.Selenide.$x;
public class ConsumersDetails extends BasePage { public class ConsumersDetails extends BasePage {
protected String consumerIdHeaderLocator = "//h1[contains(text(),'%s')]"; protected String consumerIdHeaderLocator = "//h1[contains(text(),'%s')]";
protected String topicElementLocator = "//tbody//td//a[text()='%s']"; protected String topicElementLocator = "//tbody//td//a[text()='%s']";
@Step @Step
public ConsumersDetails waitUntilScreenReady() { public ConsumersDetails waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
tableGrid.shouldBe(Condition.visible); tableGrid.shouldBe(Condition.visible);
return this; return this;
} }
@Step @Step
public boolean isRedirectedConsumerTitleVisible(String consumerGroupId) { public boolean isRedirectedConsumerTitleVisible(String consumerGroupId) {
return isVisible($x(String.format(consumerIdHeaderLocator, consumerGroupId))); return isVisible($x(String.format(consumerIdHeaderLocator, consumerGroupId)));
} }
@Step @Step
public boolean isTopicInConsumersDetailsVisible(String topicName) { public boolean isTopicInConsumersDetailsVisible(String topicName) {
tableGrid.shouldBe(Condition.visible); tableGrid.shouldBe(Condition.visible);
return isVisible($x(String.format(topicElementLocator, topicName))); return isVisible($x(String.format(topicElementLocator, topicName)));
} }
} }

View file

@ -1,17 +1,17 @@
package com.provectus.kafka.ui.pages.consumers; package com.provectus.kafka.ui.pages.consumers;
import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.CONSUMERS;
import com.codeborne.selenide.Condition; import com.codeborne.selenide.Condition;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.CONSUMERS;
public class ConsumersList extends BasePage { public class ConsumersList extends BasePage {
@Step @Step
public ConsumersList waitUntilScreenReady() { public ConsumersList waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
getPageTitleFromHeader(CONSUMERS).shouldBe(Condition.visible); getPageTitleFromHeader(CONSUMERS).shouldBe(Condition.visible);
return this; return this;
} }
} }

View file

@ -1,139 +0,0 @@
package com.provectus.kafka.ui.pages.ksqlDb;
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 org.openqa.selenium.By;
import java.util.ArrayList;
import java.util.List;
import static com.codeborne.selenide.Selenide.$;
import static com.codeborne.selenide.Selenide.$x;
import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB;
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();
getPageTitleFromHeader(KSQL_DB).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<KsqlDbList.KsqlTablesGridItem> initTablesItems() {
List<KsqlDbList.KsqlTablesGridItem> 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();
}
private List<KsqlDbList.KsqlStreamsGridItem> initStreamsItems() {
List<KsqlDbList.KsqlStreamsGridItem> 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 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();
}
}
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();
}
}
}

View file

@ -1,154 +0,0 @@
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 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();
executeBtn.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<KsqlQueryForm.KsqlResponseGridItem> initItems() {
List<KsqlQueryForm.KsqlResponseGridItem> 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();
}
}
}

View file

@ -1,17 +0,0 @@
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;
}
}

View file

@ -1,19 +0,0 @@
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;
}
}

View file

@ -0,0 +1,138 @@
package com.provectus.kafka.ui.pages.ksqldb;
import static com.codeborne.selenide.Selenide.$;
import static com.codeborne.selenide.Selenide.$x;
import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB;
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.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();
getPageTitleFromHeader(KSQL_DB).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<KsqlDbList.KsqlTablesGridItem> initTablesItems() {
List<KsqlDbList.KsqlTablesGridItem> 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();
}
private List<KsqlDbList.KsqlStreamsGridItem> initStreamsItems() {
List<KsqlDbList.KsqlStreamsGridItem> 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 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();
}
}
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();
}
}
}

View file

@ -0,0 +1,153 @@
package com.provectus.kafka.ui.pages.ksqldb;
import static com.codeborne.selenide.Condition.visible;
import static com.codeborne.selenide.Selenide.$$x;
import static com.codeborne.selenide.Selenide.$x;
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;
public class KsqlQueryForm extends BasePage {
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();
executeBtn.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<KsqlQueryForm.KsqlResponseGridItem> initItems() {
List<KsqlQueryForm.KsqlResponseGridItem> 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();
}
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,18 @@
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;
}
}

View file

@ -1,4 +1,4 @@
package com.provectus.kafka.ui.pages.ksqlDb.models; package com.provectus.kafka.ui.pages.ksqldb.models;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
@ -7,5 +7,5 @@ import lombok.experimental.Accessors;
@Accessors(chain = true) @Accessors(chain = true)
public class Stream { public class Stream {
private String name, topicName, valueFormat, partitions; private String name, topicName, valueFormat, partitions;
} }

View file

@ -1,4 +1,4 @@
package com.provectus.kafka.ui.pages.ksqlDb.models; package com.provectus.kafka.ui.pages.ksqldb.models;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
@ -7,5 +7,5 @@ import lombok.experimental.Accessors;
@Accessors(chain = true) @Accessors(chain = true)
public class Table { public class Table {
private String name, streamName; private String name, streamName;
} }

View file

@ -1,64 +1,63 @@
package com.provectus.kafka.ui.pages.panels; package com.provectus.kafka.ui.pages.panels;
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.Condition;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.provectus.kafka.ui.pages.panels.enums.MenuItem;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import com.provectus.kafka.ui.pages.panels.enums.MenuItem;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import java.time.Duration; import java.time.Duration;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; 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 { public class NaviSideBar extends BasePage {
protected SelenideElement dashboardMenuItem = $x("//a[@title='Dashboard']"); protected SelenideElement dashboardMenuItem = $x("//a[@title='Dashboard']");
protected String sideMenuOptionElementLocator = ".//ul/li[contains(.,'%s')]"; protected String sideMenuOptionElementLocator = ".//ul/li[contains(.,'%s')]";
protected String clusterElementLocator = "//aside/ul/li[contains(.,'%s')]"; protected String clusterElementLocator = "//aside/ul/li[contains(.,'%s')]";
private SelenideElement expandCluster(String clusterName) { private SelenideElement expandCluster(String clusterName) {
SelenideElement clusterElement = $x(String.format(clusterElementLocator, clusterName)).shouldBe(Condition.visible); SelenideElement clusterElement = $x(String.format(clusterElementLocator, clusterName)).shouldBe(Condition.visible);
if (clusterElement.parent().$$x(".//ul").size() == 0) { if (clusterElement.parent().$$x(".//ul").size() == 0) {
clickByActions(clusterElement); clickByActions(clusterElement);
}
return clusterElement;
} }
return clusterElement;
}
@Step @Step
public NaviSideBar waitUntilScreenReady() { public NaviSideBar waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
dashboardMenuItem.shouldBe(Condition.visible, Duration.ofSeconds(30)); dashboardMenuItem.shouldBe(Condition.visible, Duration.ofSeconds(30));
return this; return this;
} }
@Step @Step
public String getPagePath(MenuItem menuItem) { public String getPagePath(MenuItem menuItem) {
return getPagePathFromHeader(menuItem) return getPagePathFromHeader(menuItem)
.shouldBe(Condition.visible) .shouldBe(Condition.visible)
.getText().trim(); .getText().trim();
} }
@Step @Step
public NaviSideBar openSideMenu(String clusterName, MenuItem menuItem) { public NaviSideBar openSideMenu(String clusterName, MenuItem menuItem) {
clickByActions(expandCluster(clusterName).parent() clickByActions(expandCluster(clusterName).parent()
.$x(String.format(sideMenuOptionElementLocator, menuItem.getNaviTitle()))); .$x(String.format(sideMenuOptionElementLocator, menuItem.getNaviTitle())));
return this; return this;
} }
@Step @Step
public NaviSideBar openSideMenu(MenuItem menuItem) { public NaviSideBar openSideMenu(MenuItem menuItem) {
openSideMenu(CLUSTER_NAME, menuItem); openSideMenu(CLUSTER_NAME, menuItem);
return this; return this;
} }
public List<SelenideElement> getAllMenuButtons() { public List<SelenideElement> getAllMenuButtons() {
expandCluster(CLUSTER_NAME); expandCluster(CLUSTER_NAME);
return Stream.of(MenuItem.values()) return Stream.of(MenuItem.values())
.map(menuItem -> $x(String.format(sideMenuOptionElementLocator, menuItem.getNaviTitle()))) .map(menuItem -> $x(String.format(sideMenuOptionElementLocator, menuItem.getNaviTitle())))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
} }

View file

@ -1,26 +1,25 @@
package com.provectus.kafka.ui.pages.panels; package com.provectus.kafka.ui.pages.panels;
import com.codeborne.selenide.SelenideElement;
import com.provectus.kafka.ui.pages.BasePage;
import java.util.Arrays;
import java.util.List;
import static com.codeborne.selenide.Selenide.$x; import static com.codeborne.selenide.Selenide.$x;
import com.codeborne.selenide.SelenideElement;
import com.provectus.kafka.ui.pages.BasePage;
import java.util.Arrays;
import java.util.List;
public class TopPanel extends BasePage { public class TopPanel extends BasePage {
protected SelenideElement kafkaLogo = $x("//a[contains(text(),'UI for Apache Kafka')]"); protected SelenideElement kafkaLogo = $x("//a[contains(text(),'UI for Apache Kafka')]");
protected SelenideElement kafkaVersion = $x("//a[@title='Current commit']"); protected SelenideElement kafkaVersion = $x("//a[@title='Current commit']");
protected SelenideElement logOutBtn = $x("//button[contains(text(),'Log out')]"); protected SelenideElement logOutBtn = $x("//button[contains(text(),'Log out')]");
protected SelenideElement gitBtn = $x("//a[@href='https://github.com/provectus/kafka-ui']"); protected SelenideElement gitBtn = $x("//a[@href='https://github.com/provectus/kafka-ui']");
protected SelenideElement discordBtn = $x("//a[contains(@href,'https://discord.com/invite')]"); protected SelenideElement discordBtn = $x("//a[contains(@href,'https://discord.com/invite')]");
public List<SelenideElement> getAllVisibleElements() { public List<SelenideElement> getAllVisibleElements() {
return Arrays.asList(kafkaLogo, kafkaVersion, gitBtn, discordBtn); return Arrays.asList(kafkaLogo, kafkaVersion, gitBtn, discordBtn);
} }
public List<SelenideElement> getAllEnabledElements() { public List<SelenideElement> getAllEnabledElements() {
return Arrays.asList(gitBtn, discordBtn, kafkaLogo); return Arrays.asList(gitBtn, discordBtn, kafkaLogo);
} }
} }

View file

@ -1,28 +1,28 @@
package com.provectus.kafka.ui.pages.panels.enums; package com.provectus.kafka.ui.pages.panels.enums;
public enum MenuItem { public enum MenuItem {
DASHBOARD("Dashboard", "Dashboard"), DASHBOARD("Dashboard", "Dashboard"),
BROKERS("Brokers", "Brokers"), BROKERS("Brokers", "Brokers"),
TOPICS("Topics", "Topics"), TOPICS("Topics", "Topics"),
CONSUMERS("Consumers", "Consumers"), CONSUMERS("Consumers", "Consumers"),
SCHEMA_REGISTRY("Schema Registry", "Schema Registry"), SCHEMA_REGISTRY("Schema Registry", "Schema Registry"),
KAFKA_CONNECT("Kafka Connect", "Connectors"), KAFKA_CONNECT("Kafka Connect", "Connectors"),
KSQL_DB("KSQL DB", "KSQL DB"); KSQL_DB("KSQL DB", "KSQL DB");
private final String naviTitle; private final String naviTitle;
private final String pageTitle; private final String pageTitle;
MenuItem(String naviTitle, String pageTitle) { MenuItem(String naviTitle, String pageTitle) {
this.naviTitle = naviTitle; this.naviTitle = naviTitle;
this.pageTitle = pageTitle; this.pageTitle = pageTitle;
} }
public String getNaviTitle() { public String getNaviTitle() {
return naviTitle; return naviTitle;
} }
public String getPageTitle() { public String getPageTitle() {
return pageTitle; return pageTitle;
} }
} }

View file

@ -1,5 +1,10 @@
package com.provectus.kafka.ui.pages.schemas; package com.provectus.kafka.ui.pages.schemas;
import static com.codeborne.selenide.Selenide.$;
import static com.codeborne.selenide.Selenide.$$x;
import static com.codeborne.selenide.Selenide.$x;
import static org.openqa.selenium.By.id;
import com.codeborne.selenide.Condition; import com.codeborne.selenide.Condition;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.codeborne.selenide.WebDriverRunner; import com.codeborne.selenide.WebDriverRunner;
@ -7,133 +12,130 @@ import com.provectus.kafka.ui.api.model.CompatibilityLevel;
import com.provectus.kafka.ui.api.model.SchemaType; import com.provectus.kafka.ui.api.model.SchemaType;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import org.openqa.selenium.Keys;
import org.openqa.selenium.interactions.Actions;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.openqa.selenium.Keys;
import static com.codeborne.selenide.Selenide.*; import org.openqa.selenium.interactions.Actions;
import static org.openqa.selenium.By.id;
public class SchemaCreateForm extends BasePage { public class SchemaCreateForm extends BasePage {
protected SelenideElement schemaNameField = $x("//input[@name='subject']"); protected SelenideElement schemaNameField = $x("//input[@name='subject']");
protected SelenideElement pageTitle = $x("//h1['Edit']"); protected SelenideElement pageTitle = $x("//h1['Edit']");
protected SelenideElement schemaTextArea = $x("//textarea[@name='schema']"); protected SelenideElement schemaTextArea = $x("//textarea[@name='schema']");
protected SelenideElement newSchemaInput = $("#newSchema [wrap]"); protected SelenideElement newSchemaInput = $("#newSchema [wrap]");
protected SelenideElement schemaTypeDdl = $x("//ul[@name='schemaType']"); protected SelenideElement schemaTypeDdl = $x("//ul[@name='schemaType']");
protected SelenideElement compatibilityLevelList = $x("//ul[@name='compatibilityLevel']"); protected SelenideElement compatibilityLevelList = $x("//ul[@name='compatibilityLevel']");
protected SelenideElement newSchemaTextArea = $x("//div[@id='newSchema']"); protected SelenideElement newSchemaTextArea = $x("//div[@id='newSchema']");
protected SelenideElement latestSchemaTextArea = $x("//div[@id='latestSchema']"); protected SelenideElement latestSchemaTextArea = $x("//div[@id='latestSchema']");
protected SelenideElement leftVersionDdl = $(id("left-select")); protected SelenideElement leftVersionDdl = $(id("left-select"));
protected SelenideElement rightVersionDdl = $(id("right-select")); protected SelenideElement rightVersionDdl = $(id("right-select"));
protected List<SelenideElement> visibleMarkers = $$x("//div[@class='ace_scroller']//div[contains(@class,'codeMarker')]"); protected List<SelenideElement> visibleMarkers =
protected List<SelenideElement> elementsCompareVersionDdl = $$x("//ul[@role='listbox']/ul/li"); $$x("//div[@class='ace_scroller']//div[contains(@class,'codeMarker')]");
protected String ddlElementLocator = "//li[@value='%s']"; protected List<SelenideElement> elementsCompareVersionDdl = $$x("//ul[@role='listbox']/ul/li");
protected String ddlElementLocator = "//li[@value='%s']";
@Step @Step
public SchemaCreateForm waitUntilScreenReady() { public SchemaCreateForm waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
pageTitle.shouldBe(Condition.visible); pageTitle.shouldBe(Condition.visible);
return this; return this;
} }
@Step @Step
public SchemaCreateForm setSubjectName(String name) { public SchemaCreateForm setSubjectName(String name) {
schemaNameField.setValue(name); schemaNameField.setValue(name);
return this; return this;
} }
@Step @Step
public SchemaCreateForm setSchemaField(String text) { public SchemaCreateForm setSchemaField(String text) {
schemaTextArea.setValue(text); schemaTextArea.setValue(text);
return this; return this;
} }
@Step @Step
public SchemaCreateForm selectSchemaTypeFromDropdown(SchemaType schemaType) { public SchemaCreateForm selectSchemaTypeFromDropdown(SchemaType schemaType) {
schemaTypeDdl.shouldBe(Condition.enabled).click(); schemaTypeDdl.shouldBe(Condition.enabled).click();
$x(String.format(ddlElementLocator, schemaType.getValue())).shouldBe(Condition.visible).click(); $x(String.format(ddlElementLocator, schemaType.getValue())).shouldBe(Condition.visible).click();
return this; return this;
} }
@Step @Step
public SchemaCreateForm clickSubmitButton() { public SchemaCreateForm clickSubmitButton() {
clickSubmitBtn(); clickSubmitBtn();
return this; return this;
} }
@Step @Step
public SchemaCreateForm selectCompatibilityLevelFromDropdown(CompatibilityLevel.CompatibilityEnum level) { public SchemaCreateForm selectCompatibilityLevelFromDropdown(CompatibilityLevel.CompatibilityEnum level) {
compatibilityLevelList.shouldBe(Condition.enabled).click(); compatibilityLevelList.shouldBe(Condition.enabled).click();
$x(String.format(ddlElementLocator, level.getValue())).shouldBe(Condition.visible).click(); $x(String.format(ddlElementLocator, level.getValue())).shouldBe(Condition.visible).click();
return this; return this;
} }
@Step @Step
public SchemaCreateForm openLeftVersionDdl() { public SchemaCreateForm openLeftVersionDdl() {
leftVersionDdl.shouldBe(Condition.enabled).click(); leftVersionDdl.shouldBe(Condition.enabled).click();
return this; return this;
} }
@Step @Step
public SchemaCreateForm openRightVersionDdl() { public SchemaCreateForm openRightVersionDdl() {
rightVersionDdl.shouldBe(Condition.enabled).click(); rightVersionDdl.shouldBe(Condition.enabled).click();
return this; return this;
} }
@Step @Step
public int getVersionsNumberFromList() { public int getVersionsNumberFromList() {
return elementsCompareVersionDdl.size(); return elementsCompareVersionDdl.size();
} }
@Step @Step
public SchemaCreateForm selectVersionFromDropDown(int versionNumberDd) { public SchemaCreateForm selectVersionFromDropDown(int versionNumberDd) {
$x(String.format(ddlElementLocator, versionNumberDd)).shouldBe(Condition.visible).click(); $x(String.format(ddlElementLocator, versionNumberDd)).shouldBe(Condition.visible).click();
return this; return this;
} }
@Step @Step
public int getMarkedLinesNumber() { public int getMarkedLinesNumber() {
return visibleMarkers.size(); return visibleMarkers.size();
} }
@Step @Step
public SchemaCreateForm setNewSchemaValue(String configJson) { public SchemaCreateForm setNewSchemaValue(String configJson) {
newSchemaTextArea.shouldBe(Condition.visible).click(); newSchemaTextArea.shouldBe(Condition.visible).click();
newSchemaInput.shouldBe(Condition.enabled); newSchemaInput.shouldBe(Condition.enabled);
new Actions(WebDriverRunner.getWebDriver()) new Actions(WebDriverRunner.getWebDriver())
.sendKeys(Keys.PAGE_UP) .sendKeys(Keys.PAGE_UP)
.keyDown(Keys.SHIFT) .keyDown(Keys.SHIFT)
.sendKeys(Keys.PAGE_DOWN) .sendKeys(Keys.PAGE_DOWN)
.keyUp(Keys.SHIFT) .keyUp(Keys.SHIFT)
.sendKeys(Keys.DELETE) .sendKeys(Keys.DELETE)
.perform(); .perform();
setJsonInputValue(newSchemaInput, configJson); setJsonInputValue(newSchemaInput, configJson);
return this; return this;
} }
@Step @Step
public List<SelenideElement> getAllDetailsPageElements() { public List<SelenideElement> getAllDetailsPageElements() {
return Stream.of(compatibilityLevelList, newSchemaTextArea, latestSchemaTextArea, submitBtn, schemaTypeDdl) return Stream.of(compatibilityLevelList, newSchemaTextArea, latestSchemaTextArea, submitBtn, schemaTypeDdl)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Step @Step
public boolean isSubmitBtnEnabled() { public boolean isSubmitBtnEnabled() {
return isEnabled(submitBtn); return isEnabled(submitBtn);
} }
@Step @Step
public boolean isSchemaDropDownEnabled() { public boolean isSchemaDropDownEnabled() {
boolean enabled = true; boolean enabled = true;
try { try {
String attribute = schemaTypeDdl.getAttribute("disabled"); String attribute = schemaTypeDdl.getAttribute("disabled");
enabled = false; enabled = false;
} catch (Throwable ignored) { } catch (Throwable ignored) {
}
return enabled;
} }
return enabled;
}
} }

View file

@ -1,69 +1,69 @@
package com.provectus.kafka.ui.pages.schemas; package com.provectus.kafka.ui.pages.schemas;
import static com.codeborne.selenide.Selenide.$x;
import com.codeborne.selenide.Condition; import com.codeborne.selenide.Condition;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import static com.codeborne.selenide.Selenide.$x;
public class SchemaDetails extends BasePage { public class SchemaDetails extends BasePage {
protected SelenideElement actualVersionTextArea = $x("//div[@id='schema']"); protected SelenideElement actualVersionTextArea = $x("//div[@id='schema']");
protected SelenideElement compatibilityField = $x("//h4[contains(text(),'Compatibility')]/../p"); protected SelenideElement compatibilityField = $x("//h4[contains(text(),'Compatibility')]/../p");
protected SelenideElement editSchemaBtn = $x("//button[contains(text(),'Edit Schema')]"); protected SelenideElement editSchemaBtn = $x("//button[contains(text(),'Edit Schema')]");
protected SelenideElement removeBtn = $x("//*[contains(text(),'Remove')]"); protected SelenideElement removeBtn = $x("//*[contains(text(),'Remove')]");
protected SelenideElement confirmBtn = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]"); protected SelenideElement confirmBtn = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]");
protected SelenideElement schemaTypeField = $x("//h4[contains(text(),'Type')]/../p"); protected SelenideElement schemaTypeField = $x("//h4[contains(text(),'Type')]/../p");
protected SelenideElement latestVersionField = $x("//h4[contains(text(),'Latest version')]/../p"); protected SelenideElement latestVersionField = $x("//h4[contains(text(),'Latest version')]/../p");
protected SelenideElement compareVersionBtn = $x("//button[text()='Compare Versions']"); protected SelenideElement compareVersionBtn = $x("//button[text()='Compare Versions']");
protected String schemaHeaderLocator = "//h1[contains(text(),'%s')]"; protected String schemaHeaderLocator = "//h1[contains(text(),'%s')]";
@Step @Step
public SchemaDetails waitUntilScreenReady() { public SchemaDetails waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
actualVersionTextArea.shouldBe(Condition.visible); actualVersionTextArea.shouldBe(Condition.visible);
return this; return this;
} }
@Step @Step
public String getCompatibility() { public String getCompatibility() {
return compatibilityField.getText(); return compatibilityField.getText();
} }
@Step @Step
public boolean isSchemaHeaderVisible(String schemaName) { public boolean isSchemaHeaderVisible(String schemaName) {
return isVisible($x(String.format(schemaHeaderLocator, schemaName))); return isVisible($x(String.format(schemaHeaderLocator, schemaName)));
} }
@Step @Step
public int getLatestVersion() { public int getLatestVersion() {
return Integer.parseInt(latestVersionField.getText()); return Integer.parseInt(latestVersionField.getText());
} }
@Step @Step
public String getSchemaType() { public String getSchemaType() {
return schemaTypeField.getText(); return schemaTypeField.getText();
} }
@Step @Step
public SchemaDetails openEditSchema() { public SchemaDetails openEditSchema() {
editSchemaBtn.shouldBe(Condition.visible).click(); editSchemaBtn.shouldBe(Condition.visible).click();
return this; return this;
} }
@Step @Step
public SchemaDetails openCompareVersionMenu() { public SchemaDetails openCompareVersionMenu() {
compareVersionBtn.shouldBe(Condition.enabled).click(); compareVersionBtn.shouldBe(Condition.enabled).click();
return this; return this;
} }
@Step @Step
public SchemaDetails removeSchema() { public SchemaDetails removeSchema() {
clickByJavaScript(dotMenuBtn); clickByJavaScript(dotMenuBtn);
removeBtn.shouldBe(Condition.enabled).click(); removeBtn.shouldBe(Condition.enabled).click();
confirmBtn.shouldBe(Condition.visible).click(); confirmBtn.shouldBe(Condition.visible).click();
confirmBtn.shouldBe(Condition.disappear); confirmBtn.shouldBe(Condition.disappear);
return this; return this;
} }
} }

View file

@ -1,42 +1,42 @@
package com.provectus.kafka.ui.pages.schemas; package com.provectus.kafka.ui.pages.schemas;
import static com.codeborne.selenide.Selenide.$x;
import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.SCHEMA_REGISTRY;
import com.codeborne.selenide.Condition; import com.codeborne.selenide.Condition;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import static com.codeborne.selenide.Selenide.$x;
import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.SCHEMA_REGISTRY;
public class SchemaRegistryList extends BasePage { public class SchemaRegistryList extends BasePage {
protected SelenideElement createSchemaBtn = $x("//button[contains(text(),'Create Schema')]"); protected SelenideElement createSchemaBtn = $x("//button[contains(text(),'Create Schema')]");
@Step @Step
public SchemaRegistryList waitUntilScreenReady() { public SchemaRegistryList waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
getPageTitleFromHeader(SCHEMA_REGISTRY).shouldBe(Condition.visible); getPageTitleFromHeader(SCHEMA_REGISTRY).shouldBe(Condition.visible);
return this; return this;
} }
@Step @Step
public SchemaRegistryList clickCreateSchema() { public SchemaRegistryList clickCreateSchema() {
clickByJavaScript(createSchemaBtn); clickByJavaScript(createSchemaBtn);
return this; return this;
} }
@Step @Step
public SchemaRegistryList openSchema(String schemaName) { public SchemaRegistryList openSchema(String schemaName) {
getTableElement(schemaName) getTableElement(schemaName)
.shouldBe(Condition.enabled).click(); .shouldBe(Condition.enabled).click();
return this; return this;
} }
@Step @Step
public boolean isSchemaVisible(String schemaName) { public boolean isSchemaVisible(String schemaName) {
tableGrid.shouldBe(Condition.visible); tableGrid.shouldBe(Condition.visible);
return isVisible(getTableElement(schemaName)); return isVisible(getTableElement(schemaName));
} }
} }

View file

@ -1,57 +1,56 @@
package com.provectus.kafka.ui.pages.topics; package com.provectus.kafka.ui.pages.topics;
import static com.codeborne.selenide.Selenide.$x;
import static com.codeborne.selenide.Selenide.refresh;
import com.codeborne.selenide.Condition; import com.codeborne.selenide.Condition;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import java.util.Arrays; import java.util.Arrays;
import static com.codeborne.selenide.Selenide.$x;
import static com.codeborne.selenide.Selenide.refresh;
public class ProduceMessagePanel extends BasePage { public class ProduceMessagePanel extends BasePage {
protected SelenideElement keyTextArea = $x("//div[@id='key']/textarea"); protected SelenideElement keyTextArea = $x("//div[@id='key']/textarea");
protected SelenideElement contentTextArea = $x("//div[@id='content']/textarea"); protected SelenideElement valueTextArea = $x("//div[@id='content']/textarea");
protected SelenideElement headersTextArea = $x("//div[@id='headers']/textarea"); protected SelenideElement headersTextArea = $x("//div[@id='headers']/textarea");
protected SelenideElement submitBtn = headersTextArea.$x("../../../..//button[@type='submit']"); protected SelenideElement submitBtn = headersTextArea.$x("../../../..//button[@type='submit']");
protected SelenideElement partitionDdl = $x("//ul[@name='partition']"); protected SelenideElement partitionDdl = $x("//ul[@name='partition']");
protected SelenideElement keySerdeDdl = $x("//ul[@name='keySerde']"); protected SelenideElement keySerdeDdl = $x("//ul[@name='keySerde']");
protected SelenideElement contentSerdeDdl = $x("//ul[@name='valueSerde']"); protected SelenideElement contentSerdeDdl = $x("//ul[@name='valueSerde']");
@Step @Step
public ProduceMessagePanel waitUntilScreenReady() { public ProduceMessagePanel waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
Arrays.asList(partitionDdl, keySerdeDdl, contentSerdeDdl).forEach(element -> element.shouldBe(Condition.visible)); Arrays.asList(partitionDdl, keySerdeDdl, contentSerdeDdl).forEach(element -> element.shouldBe(Condition.visible));
return this; return this;
} }
@Step @Step
public ProduceMessagePanel setKeyField(String value) { public ProduceMessagePanel setKeyField(String value) {
clearByKeyboard(keyTextArea); clearByKeyboard(keyTextArea);
keyTextArea.setValue(value); keyTextArea.setValue(value);
return this; return this;
} }
@Step @Step
public ProduceMessagePanel setContentFiled(String value) { public ProduceMessagePanel setValueFiled(String value) {
clearByKeyboard(contentTextArea); clearByKeyboard(valueTextArea);
contentTextArea.setValue(value); valueTextArea.setValue(value);
return this; return this;
} }
@Step @Step
public ProduceMessagePanel setHeaderFiled(String value) { public ProduceMessagePanel setHeadersFld(String value) {
headersTextArea.setValue(value); headersTextArea.setValue(value);
return this; return this;
} }
@Step @Step
public ProduceMessagePanel submitProduceMessage() { public ProduceMessagePanel submitProduceMessage() {
clickByActions(submitBtn); clickByActions(submitBtn);
submitBtn.shouldBe(Condition.disappear); submitBtn.shouldBe(Condition.disappear);
refresh(); refresh();
return this; return this;
} }
} }

View file

@ -1,6 +1,15 @@
package com.provectus.kafka.ui.pages.topics; package com.provectus.kafka.ui.pages.topics;
import com.codeborne.selenide.*; import static com.codeborne.selenide.Selenide.$;
import static com.codeborne.selenide.Selenide.$$;
import static com.codeborne.selenide.Selenide.$x;
import static org.openqa.selenium.By.id;
import com.codeborne.selenide.ClickOptions;
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 com.provectus.kafka.ui.pages.BasePage;
import com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue; 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.CustomParameterType;
@ -8,269 +17,264 @@ import com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk;
import com.provectus.kafka.ui.pages.topics.enums.TimeToRetain; import com.provectus.kafka.ui.pages.topics.enums.TimeToRetain;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import static com.codeborne.selenide.Selenide.*;
public class TopicCreateEditForm extends BasePage { public class TopicCreateEditForm extends BasePage {
protected SelenideElement timeToRetainField = $x("//input[@id='timeToRetain']"); protected SelenideElement timeToRetainField = $x("//input[@id='timeToRetain']");
protected SelenideElement partitionsField = $x("//input[@name='partitions']"); protected SelenideElement partitionsField = $x("//input[@name='partitions']");
protected SelenideElement nameField = $x("//input[@name='name']"); protected SelenideElement nameField = $(id("topicFormName"));
protected SelenideElement maxMessageBytesField = $x("//input[@name='maxMessageBytes']"); protected SelenideElement maxMessageBytesField = $x("//input[@name='maxMessageBytes']");
protected SelenideElement minInSyncReplicasField = $x("//input[@name='minInSyncReplicas']"); protected SelenideElement minInSyncReplicasField = $x("//input[@name='minInSyncReplicas']");
protected SelenideElement cleanUpPolicyDdl = $x("//ul[@id='topicFormCleanupPolicy']"); protected SelenideElement cleanUpPolicyDdl = $x("//ul[@id='topicFormCleanupPolicy']");
protected SelenideElement maxSizeOnDiscDdl = $x("//ul[@id='topicFormRetentionBytes']"); protected SelenideElement maxSizeOnDiscDdl = $x("//ul[@id='topicFormRetentionBytes']");
protected SelenideElement customParameterDdl = $x("//ul[contains(@name,'customParams')]"); protected SelenideElement customParameterDdl = $x("//ul[contains(@name,'customParams')]");
protected SelenideElement deleteCustomParameterBtn = $x("//span[contains(@title,'Delete customParam')]"); protected SelenideElement deleteCustomParameterBtn = $x("//span[contains(@title,'Delete customParam')]");
protected SelenideElement addCustomParameterTypeBtn = $x("//button[contains(text(),'Add Custom Parameter')]"); protected SelenideElement addCustomParameterTypeBtn = $x("//button[contains(text(),'Add Custom Parameter')]");
protected SelenideElement customParameterValueField = $x("//input[@placeholder='Value']"); protected SelenideElement customParameterValueField = $x("//input[@placeholder='Value']");
protected SelenideElement validationCustomParameterValueMsg = $x("//p[contains(text(),'Value is required')]"); protected SelenideElement validationCustomParameterValueMsg = $x("//p[contains(text(),'Value is required')]");
protected String ddlElementLocator = "//li[@value='%s']"; protected String ddlElementLocator = "//li[@value='%s']";
protected String btnTimeToRetainLocator = "//button[@class][text()='%s']"; protected String btnTimeToRetainLocator = "//button[@class][text()='%s']";
@Step @Step
public TopicCreateEditForm waitUntilScreenReady() { public TopicCreateEditForm waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
nameField.shouldBe(Condition.visible); nameField.shouldBe(Condition.visible);
return this; 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) {
sendKeysAfterClear(nameField, 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 clickSaveTopicBtn() {
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 boolean isCreateTopicButtonEnabled() { public KafkaUiSelectElement(SelenideElement selectElement) {
return isEnabled(submitBtn); this.selectElement = selectElement;
} }
public boolean isDeleteCustomParameterButtonEnabled() { public void selectByOptionValue(String optionValue) {
return isEnabled(deleteCustomParameterBtn); selectElement.click();
selectElement
.$$x(".//ul/li[@role='option']")
.find(Condition.attribute("value", optionValue))
.click(ClickOptions.usingJavaScript());
} }
public boolean isNameFieldEnabled() { public void selectByVisibleText(String visibleText) {
return isEnabled(nameField); selectElement.click();
selectElement
.$$("ul>li[role=option]")
.find(Condition.exactText(visibleText))
.click();
} }
@Step public String getCurrentValue() {
public TopicCreateEditForm setTopicName(String topicName) { return selectElement.$("li").getText();
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();
}
} }
}
} }

View file

@ -1,463 +1,475 @@
package com.provectus.kafka.ui.pages.topics; package com.provectus.kafka.ui.pages.topics;
import static com.codeborne.selenide.Selenide.$$x;
import static com.codeborne.selenide.Selenide.$x;
import static com.codeborne.selenide.Selenide.sleep;
import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.OVERVIEW;
import static org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils.nextInt;
import com.codeborne.selenide.CollectionCondition; import com.codeborne.selenide.CollectionCondition;
import com.codeborne.selenide.Condition; import com.codeborne.selenide.Condition;
import com.codeborne.selenide.ElementsCollection; import com.codeborne.selenide.ElementsCollection;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import org.openqa.selenium.By;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime; import java.time.LocalTime;
import java.time.YearMonth; import java.time.YearMonth;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeFormatterBuilder;
import java.util.*; import java.util.ArrayList;
import java.util.Arrays;
import static com.codeborne.selenide.Selenide.*; import java.util.Collections;
import static org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils.nextInt; import java.util.List;
import java.util.Locale;
import java.util.Objects;
public class TopicDetails extends BasePage { public class TopicDetails extends BasePage {
protected SelenideElement clearMessagesBtn = $x(("//div[contains(text(), 'Clear messages')]")); protected SelenideElement clearMessagesBtn = $x(("//div[contains(text(), 'Clear messages')]"));
protected SelenideElement recreateTopicBtn = $x("//div[text()='Recreate Topic']"); protected SelenideElement recreateTopicBtn = $x("//div[text()='Recreate Topic']");
protected SelenideElement messageAmountCell = $x("//tbody/tr/td[5]"); protected SelenideElement messageAmountCell = $x("//tbody/tr/td[5]");
protected SelenideElement overviewTab = $x("//a[contains(text(),'Overview')]"); protected SelenideElement overviewTab = $x("//a[contains(text(),'Overview')]");
protected SelenideElement messagesTab = $x("//a[contains(text(),'Messages')]"); protected SelenideElement messagesTab = $x("//a[contains(text(),'Messages')]");
protected SelenideElement seekTypeDdl = $x("//ul[@id='selectSeekType']/li"); protected SelenideElement seekTypeDdl = $x("//ul[@id='selectSeekType']//li");
protected SelenideElement seekTypeField = $x("//label[text()='Seek Type']//..//div/input"); protected SelenideElement seekTypeField = $x("//label[text()='Seek Type']//..//div/input");
protected SelenideElement addFiltersBtn = $x("//button[text()='Add Filters']"); protected SelenideElement addFiltersBtn = $x("//button[text()='Add Filters']");
protected SelenideElement savedFiltersLink = $x("//div[text()='Saved Filters']"); protected SelenideElement savedFiltersLink = $x("//div[text()='Saved Filters']");
protected SelenideElement addFilterCodeModalTitle = $x("//label[text()='Filter code']"); protected SelenideElement addFilterCodeModalTitle = $x("//label[text()='Filter code']");
protected SelenideElement addFilterCodeInput = $x("//div[@id='ace-editor']//textarea"); protected SelenideElement addFilterCodeInput = $x("//div[@id='ace-editor']//textarea");
protected SelenideElement saveThisFilterCheckBoxAddFilterMdl = $x("//input[@name='saveFilter']"); protected SelenideElement saveThisFilterCheckBoxAddFilterMdl = $x("//input[@name='saveFilter']");
protected SelenideElement displayNameInputAddFilterMdl = $x("//input[@placeholder='Enter Name']"); protected SelenideElement displayNameInputAddFilterMdl = $x("//input[@placeholder='Enter Name']");
protected SelenideElement cancelBtnAddFilterMdl = $x("//button[text()='Cancel']"); protected SelenideElement cancelBtnAddFilterMdl = $x("//button[text()='Cancel']");
protected SelenideElement addFilterBtnAddFilterMdl = $x("//button[text()='Add filter']"); protected SelenideElement addFilterBtnAddFilterMdl = $x("//button[text()='Add filter']");
protected SelenideElement addFiltersBtnMessages = $x("//button[text()='Add Filters']"); protected SelenideElement addFiltersBtnMessages = $x("//button[text()='Add Filters']");
protected SelenideElement selectFilterBtnAddFilterMdl = $x("//button[text()='Select filter']"); protected SelenideElement selectFilterBtnAddFilterMdl = $x("//button[text()='Select filter']");
protected SelenideElement editSettingsMenu = $x("//li[@role][contains(text(),'Edit settings')]"); protected SelenideElement editSettingsMenu = $x("//li[@role][contains(text(),'Edit settings')]");
protected SelenideElement removeTopicBtn = $x("//ul[@role='menu']//div[contains(text(),'Remove Topic')]"); protected SelenideElement removeTopicBtn = $x("//ul[@role='menu']//div[contains(text(),'Remove Topic')]");
protected SelenideElement produceMessageBtn = $x("//div//button[text()='Produce Message']"); protected SelenideElement produceMessageBtn = $x("//div//button[text()='Produce Message']");
protected SelenideElement contentMessageTab = $x("//html//div[@id='root']/div/main//table//p"); 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 cleanUpPolicyField = $x("//div[contains(text(),'Clean Up Policy')]/../span/*");
protected SelenideElement partitionsField = $x("//div[contains(text(),'Partitions')]/../span"); protected SelenideElement partitionsField = $x("//div[contains(text(),'Partitions')]/../span");
protected SelenideElement backToCreateFiltersLink = $x("//div[text()='Back To create filters']"); protected SelenideElement backToCreateFiltersLink = $x("//div[text()='Back To create filters']");
protected ElementsCollection messageGridItems = $$x("//tbody//tr"); protected ElementsCollection messageGridItems = $$x("//tbody//tr");
protected SelenideElement actualCalendarDate = $x("//div[@class='react-datepicker__current-month']"); protected SelenideElement actualCalendarDate = $x("//div[@class='react-datepicker__current-month']");
protected SelenideElement previousMonthButton = $x("//button[@aria-label='Previous Month']"); protected SelenideElement previousMonthButton = $x("//button[@aria-label='Previous Month']");
protected SelenideElement nextMonthButton = $x("//button[@aria-label='Next Month']"); protected SelenideElement nextMonthButton = $x("//button[@aria-label='Next Month']");
protected SelenideElement calendarTimeFld = $x("//input[@placeholder='Time']"); protected SelenideElement calendarTimeFld = $x("//input[@placeholder='Time']");
protected String dayCellLtr = "//div[@role='option'][contains(text(),'%d')]"; protected String detailsTabLtr = "//nav//a[contains(text(),'%s')]";
protected String seekFilterDdlLocator = "//ul[@id='selectSeekType']/ul/li[text()='%s']"; protected String dayCellLtr = "//div[@role='option'][contains(text(),'%d')]";
protected String savedFilterNameLocator = "//div[@role='savedFilter']/div[contains(text(),'%s')]"; protected String seekFilterDdlLocator = "//ul[@id='selectSeekType']/ul/li[text()='%s']";
protected String consumerIdLocator = "//a[@title='%s']"; protected String savedFilterNameLocator = "//div[@role='savedFilter']/div[contains(text(),'%s')]";
protected String topicHeaderLocator = "//h1[contains(text(),'%s')]"; protected String consumerIdLocator = "//a[@title='%s']";
protected String activeFilterNameLocator = "//div[@data-testid='activeSmartFilter'][contains(text(),'%s')]"; protected String topicHeaderLocator = "//h1[contains(text(),'%s')]";
protected String settingsGridValueLocator = "//tbody/tr/td/span[text()='%s']//ancestor::tr/td[2]/span"; 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 @Step
public TopicDetails waitUntilScreenReady() { public TopicDetails waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
overviewTab.shouldBe(Condition.visible); $x(String.format(detailsTabLtr, OVERVIEW)).shouldBe(Condition.visible);
return this; return this;
}
@Step
public TopicDetails openDetailsTab(TopicMenu menu) {
$x(String.format(detailsTabLtr, menu.toString())).shouldBe(Condition.enabled).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 boolean isClearMessagesMenuEnabled() {
return !Objects.requireNonNull(clearMessagesBtn.shouldBe(Condition.visible)
.$x("./..").getAttribute("class"))
.contains("disabled");
}
@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<SelenideElement> getAllAddFilterModalVisibleElements() {
return Arrays.asList(savedFiltersLink, displayNameInputAddFilterMdl, addFilterBtnAddFilterMdl,
cancelBtnAddFilterMdl);
}
public List<SelenideElement> getAllAddFilterModalEnabledElements() {
return Arrays.asList(displayNameInputAddFilterMdl, cancelBtnAddFilterMdl);
}
public List<SelenideElement> getAllAddFilterModalDisabledElements() {
return Collections.singletonList(addFilterBtnAddFilterMdl);
}
@Step
public TopicDetails openConsumerGroup(String consumerId) {
$x(String.format(consumerIdLocator, consumerId)).click();
return this;
}
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<TopicDetails.MessageGridItem> initItems() {
List<TopicDetails.MessageGridItem> 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 TopicDetails.MessageGridItem getMessageByKey(String key) {
return initItems().stream()
.filter(e -> e.getKey().equals(key))
.findFirst().orElseThrow();
}
@Step
public List<MessageGridItem> 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 @Step
public TopicDetails openDetailsTab(TopicMenu menu) { public MessageGridItem clickExpand() {
$(By.linkText(menu.toString())).shouldBe(Condition.visible).click(); clickByJavaScript(element.$x("./td[1]/span"));
waitUntilSpinnerDisappear(); return this;
return this; }
private SelenideElement getOffsetElm() {
return element.$x("./td[2]");
} }
@Step @Step
public String getSettingsGridValueByKey(String key) { public int getOffset() {
return $x(String.format(settingsGridValueLocator, key)).scrollTo().shouldBe(Condition.visible).getText(); return Integer.parseInt(getOffsetElm().getText().trim());
} }
@Step @Step
public TopicDetails openDotMenu() { public int getPartition() {
clickByJavaScript(dotMenuBtn); return Integer.parseInt(element.$x("./td[3]").getText().trim());
return this;
} }
@Step @Step
public boolean isAlertWithMessageVisible(AlertHeader header, String message) { public LocalDateTime getTimestamp() {
return isAlertVisible(header, message); 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 @Step
public TopicDetails clickEditSettingsMenu() { public String getKey() {
editSettingsMenu.shouldBe(Condition.visible).click(); return element.$x("./td[5]").getText().trim();
return this;
} }
@Step @Step
public boolean isConfirmationMdlVisible() { public String getValue() {
return isConfirmationModalVisible(); return element.$x("./td[6]").getAttribute("title");
} }
@Step @Step
public TopicDetails clickClearMessagesMenu() { public MessageGridItem openDotMenu() {
clearMessagesBtn.shouldBe(Condition.visible).click(); getOffsetElm().hover();
return this; element.$x("./td[7]/div/button[@aria-label='Dropdown Toggle']")
} .shouldBe(Condition.visible).click();
@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; return this;
} }
@Step @Step
public TopicDetails openSavedFiltersListMdl() { public MessageGridItem clickCopyToClipBoard() {
savedFiltersLink.shouldBe(Condition.enabled).click(); clickByJavaScript(element.$x("./td[7]//li[text() = 'Copy to clipboard']")
backToCreateFiltersLink.shouldBe(Condition.visible); .shouldBe(Condition.visible));
return this; return this;
} }
@Step @Step
public boolean isFilterVisibleAtSavedFiltersMdl(String filterName) { public MessageGridItem clickSaveAsFile() {
return isVisible($x(String.format(savedFilterNameLocator, filterName))); clickByJavaScript(element.$x("./td[7]//li[text() = 'Save as a file']")
} .shouldBe(Condition.visible));
return this;
@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<SelenideElement> getAllAddFilterModalVisibleElements() {
return Arrays.asList(savedFiltersLink, displayNameInputAddFilterMdl, addFilterBtnAddFilterMdl, cancelBtnAddFilterMdl);
}
public List<SelenideElement> getAllAddFilterModalEnabledElements() {
return Arrays.asList(displayNameInputAddFilterMdl, cancelBtnAddFilterMdl);
}
public List<SelenideElement> 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<TopicDetails.MessageGridItem> initItems() {
List<TopicDetails.MessageGridItem> 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<MessageGridItem> 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;
}
} }
}
} }

View file

@ -1,66 +1,65 @@
package com.provectus.kafka.ui.pages.topics; package com.provectus.kafka.ui.pages.topics;
import static com.codeborne.selenide.Selenide.$x;
import com.codeborne.selenide.CollectionCondition; import com.codeborne.selenide.CollectionCondition;
import com.codeborne.selenide.Condition; import com.codeborne.selenide.Condition;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static com.codeborne.selenide.Selenide.$x;
public class TopicSettingsTab extends BasePage { public class TopicSettingsTab extends BasePage {
protected SelenideElement defaultValueColumnHeaderLocator = $x("//div[text() = 'Default Value']"); protected SelenideElement defaultValueColumnHeaderLocator = $x("//div[text() = 'Default Value']");
@Step @Step
public TopicSettingsTab waitUntilScreenReady() { public TopicSettingsTab waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
defaultValueColumnHeaderLocator.shouldBe(Condition.visible); defaultValueColumnHeaderLocator.shouldBe(Condition.visible);
return this; return this;
} }
private List<SettingsGridItem> initGridItems() { private List<SettingsGridItem> initGridItems() {
List<SettingsGridItem> gridItemList = new ArrayList<>(); List<SettingsGridItem> gridItemList = new ArrayList<>();
gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))
.forEach(item -> gridItemList.add(new SettingsGridItem(item))); .forEach(item -> gridItemList.add(new SettingsGridItem(item)));
return gridItemList; return gridItemList;
} }
private TopicSettingsTab.SettingsGridItem getItemByKey(String key) { private TopicSettingsTab.SettingsGridItem getItemByKey(String key) {
return initGridItems().stream() return initGridItems().stream()
.filter(e -> e.getKey().equals(key)) .filter(e -> e.getKey().equals(key))
.findFirst().orElseThrow(); .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 @Step
public String getValueByKey(String key) { public String getKey() {
return getItemByKey(key).getValue(); return element.$x("./td[1]/span").getText().trim();
} }
public static class SettingsGridItem extends BasePage { @Step
public String getValue() {
private final SelenideElement element; return element.$x("./td[2]/span").getText().trim();
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();
}
} }
@Step
public String getDefaultValue() {
return element.$x("./td[3]/span").getText().trim();
}
}
} }

View file

@ -1,265 +1,283 @@
package com.provectus.kafka.ui.pages.topics; package com.provectus.kafka.ui.pages.topics;
import static com.codeborne.selenide.Condition.visible;
import static com.codeborne.selenide.Selenide.$x;
import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.TOPICS;
import com.codeborne.selenide.CollectionCondition; import com.codeborne.selenide.CollectionCondition;
import com.codeborne.selenide.Condition; import com.codeborne.selenide.Condition;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.BasePage;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static com.codeborne.selenide.Condition.visible;
import static com.codeborne.selenide.Selenide.$x;
import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.TOPICS;
public class TopicsList extends BasePage { public class TopicsList extends BasePage {
protected SelenideElement addTopicBtn = $x("//button[normalize-space(text()) ='Add a Topic']"); protected SelenideElement addTopicBtn = $x("//button[normalize-space(text()) ='Add a Topic']");
protected SelenideElement searchField = $x("//input[@placeholder='Search by Topic Name']"); protected SelenideElement searchField = $x("//input[@placeholder='Search by Topic Name']");
protected SelenideElement showInternalRadioBtn = $x("//input[@name='ShowInternalTopics']"); protected SelenideElement showInternalRadioBtn = $x("//input[@name='ShowInternalTopics']");
protected SelenideElement deleteSelectedTopicsBtn = $x("//button[text()='Delete selected topics']"); protected SelenideElement deleteSelectedTopicsBtn = $x("//button[text()='Delete selected topics']");
protected SelenideElement copySelectedTopicBtn = $x("//button[text()='Copy selected topic']"); protected SelenideElement copySelectedTopicBtn = $x("//button[text()='Copy selected topic']");
protected SelenideElement purgeMessagesOfSelectedTopicsBtn = $x("//button[text()='Purge messages of selected topics']"); protected SelenideElement purgeMessagesOfSelectedTopicsBtn =
protected SelenideElement clearMessagesBtn = $x("//ul[contains(@class ,'open')]//div[text()='Clear Messages']"); $x("//button[text()='Purge messages of selected topics']");
protected SelenideElement recreateTopicBtn = $x("//ul[contains(@class ,'open')]//div[text()='Recreate Topic']"); protected SelenideElement clearMessagesBtn = $x("//ul[contains(@class ,'open')]//div[text()='Clear Messages']");
protected SelenideElement removeTopicBtn = $x("//ul[contains(@class ,'open')]//div[text()='Remove Topic']"); 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 @Step
public TopicsList waitUntilScreenReady() { public TopicsList waitUntilScreenReady() {
waitUntilSpinnerDisappear(); waitUntilSpinnerDisappear();
getPageTitleFromHeader(TOPICS).shouldBe(visible); getPageTitleFromHeader(TOPICS).shouldBe(visible);
return this; 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) {
if (select) {
if (!showInternalRadioBtn.isSelected()) {
clickByJavaScript(showInternalRadioBtn);
waitUntilSpinnerDisappear(1);
}
} else {
if (showInternalRadioBtn.isSelected()) {
clickByJavaScript(showInternalRadioBtn);
waitUntilSpinnerDisappear(1);
}
}
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<SelenideElement> 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<SelenideElement> 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<SelenideElement> 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<SelenideElement> getAllVisibleElements() {
List<SelenideElement> visibleElements = new ArrayList<>(getVisibleColumnHeaders());
visibleElements.addAll(Arrays.asList(searchField, addTopicBtn, tableGrid));
visibleElements.addAll(getActionButtons());
return visibleElements;
}
@Step
public List<SelenideElement> getAllEnabledElements() {
List<SelenideElement> enabledElements = new ArrayList<>(getEnabledColumnHeaders());
enabledElements.addAll(Arrays.asList(searchField, showInternalRadioBtn, addTopicBtn));
return enabledElements;
}
private List<TopicGridItem> initGridItems() {
List<TopicGridItem> gridItemList = new ArrayList<>();
gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))
.forEach(item -> gridItemList.add(new TopicGridItem(item)));
return gridItemList;
}
@Step
public TopicGridItem getTopicItem(String name) {
TopicGridItem topicGridItem = initGridItems().stream()
.filter(e -> e.getName().equals(name))
.findFirst().orElse(null);
if (topicGridItem == null) {
searchItem(name);
topicGridItem = initGridItems().stream()
.filter(e -> e.getName().equals(name))
.findFirst().orElseThrow();
}
return topicGridItem;
}
@Step
public TopicGridItem getAnyNonInternalTopic() {
return getNonInternalTopics().stream()
.findAny().orElseThrow();
}
@Step
public List<TopicGridItem> getNonInternalTopics() {
return initGridItems().stream()
.filter(e -> !e.isInternal())
.collect(Collectors.toList());
}
@Step
public List<TopicGridItem> 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 @Step
public TopicsList clickAddTopicBtn() { public TopicsList selectItem(boolean select) {
clickByJavaScript(addTopicBtn); selectElement(element.$x("./td[1]/input"), select);
return this; return new TopicsList();
}
private SelenideElement getNameElm() {
return element.$x("./td[2]");
} }
@Step @Step
public boolean isTopicVisible(String topicName) { public boolean isInternal() {
tableGrid.shouldBe(visible); boolean internal = false;
return isVisible(getTableElement(topicName)); try {
internal = getNameElm().$x("./a/span").isDisplayed();
} catch (Throwable ignored) {
}
return internal;
} }
@Step @Step
public boolean isShowInternalRadioBtnSelected() { public String getName() {
return isSelected(showInternalRadioBtn); return getNameElm().$x("./a").getAttribute("title");
} }
@Step @Step
public TopicsList setShowInternalRadioButton(boolean select) { public void openItem() {
selectElement(showInternalRadioBtn, select); getNameElm().click();
return this;
} }
@Step @Step
public TopicsList openTopic(String topicName) { public int getPartition() {
getTopicItem(topicName).openItem(); return Integer.parseInt(element.$x("./td[3]").getText().trim());
return this;
} }
@Step @Step
public TopicsList openDotMenuByTopicName(String topicName) { public int getOutOfSyncReplicas() {
getTopicItem(topicName).openDotMenu(); return Integer.parseInt(element.$x("./td[4]").getText().trim());
return this;
} }
@Step @Step
public boolean isCopySelectedTopicBtnEnabled() { public int getReplicationFactor() {
return isEnabled(copySelectedTopicBtn); return Integer.parseInt(element.$x("./td[5]").getText().trim());
} }
@Step @Step
public List<SelenideElement> getActionButtons() { public int getNumberOfMessages() {
return Stream.of(deleteSelectedTopicsBtn, copySelectedTopicBtn, purgeMessagesOfSelectedTopicsBtn) return Integer.parseInt(element.$x("./td[6]").getText().trim());
.collect(Collectors.toList());
} }
@Step @Step
public TopicsList clickCopySelectedTopicBtn() { public int getSize() {
copySelectedTopicBtn.shouldBe(Condition.enabled).click(); return Integer.parseInt(element.$x("./td[7]").getText().trim());
return this;
} }
@Step @Step
public TopicsList clickPurgeMessagesOfSelectedTopicsBtn() { public void openDotMenu() {
purgeMessagesOfSelectedTopicsBtn.shouldBe(Condition.enabled).click(); element.$x("./td[8]//button").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<SelenideElement> 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<SelenideElement> 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<SelenideElement> getAllVisibleElements() {
List<SelenideElement> visibleElements = new ArrayList<>(getVisibleColumnHeaders());
visibleElements.addAll(Arrays.asList(searchField, addTopicBtn, tableGrid));
visibleElements.addAll(getActionButtons());
return visibleElements;
}
@Step
public List<SelenideElement> getAllEnabledElements() {
List<SelenideElement> enabledElements = new ArrayList<>(getEnabledColumnHeaders());
enabledElements.addAll(Arrays.asList(searchField, showInternalRadioBtn, addTopicBtn));
return enabledElements;
}
private List<TopicGridItem> initGridItems() {
List<TopicGridItem> 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 TopicGridItem getAnyNonInternalTopic() {
return getNonInternalTopics().stream()
.findAny().orElseThrow();
}
@Step
public List<TopicGridItem> getNonInternalTopics() {
return initGridItems().stream()
.filter(e -> !e.isInternal())
.collect(Collectors.toList());
}
@Step
public List<TopicGridItem> 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();
}
private SelenideElement getNameElm() {
return element.$x("./td[2]");
}
@Step
public boolean isInternal() {
boolean internal = false;
try {
internal = getNameElm().$x("./a/span").isDisplayed();
} catch (Throwable ignored) {
}
return internal;
}
@Step
public String getName() {
return getNameElm().$x("./a").getAttribute("title");
}
@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();
}
} }
}
} }

View file

@ -2,24 +2,24 @@ package com.provectus.kafka.ui.pages.topics.enums;
public enum CleanupPolicyValue { public enum CleanupPolicyValue {
DELETE("delete", "Delete"), DELETE("delete", "Delete"),
COMPACT("compact", "Compact"), COMPACT("compact", "Compact"),
COMPACT_DELETE("compact,delete", "Compact,Delete"); COMPACT_DELETE("compact,delete", "Compact,Delete");
private final String optionValue; private final String optionValue;
private final String visibleText; private final String visibleText;
CleanupPolicyValue(String optionValue, String visibleText) { CleanupPolicyValue(String optionValue, String visibleText) {
this.optionValue = optionValue; this.optionValue = optionValue;
this.visibleText = visibleText; this.visibleText = visibleText;
} }
public String getOptionValue() { public String getOptionValue() {
return optionValue; return optionValue;
} }
public String getVisibleText() { public String getVisibleText() {
return visibleText; return visibleText;
} }
} }

View file

@ -2,36 +2,36 @@ package com.provectus.kafka.ui.pages.topics.enums;
public enum CustomParameterType { public enum CustomParameterType {
COMPRESSION_TYPE("compression.type"), COMPRESSION_TYPE("compression.type"),
DELETE_RETENTION_MS("delete.retention.ms"), DELETE_RETENTION_MS("delete.retention.ms"),
FILE_DELETE_DELAY_MS("file.delete.delay.ms"), FILE_DELETE_DELAY_MS("file.delete.delay.ms"),
FLUSH_MESSAGES("flush.messages"), FLUSH_MESSAGES("flush.messages"),
FLUSH_MS("flush.ms"), FLUSH_MS("flush.ms"),
FOLLOWER_REPLICATION_THROTTLED_REPLICAS("follower.replication.throttled.replicas"), FOLLOWER_REPLICATION_THROTTLED_REPLICAS("follower.replication.throttled.replicas"),
INDEX_INTERVAL_BYTES("index.interval.bytes"), INDEX_INTERVAL_BYTES("index.interval.bytes"),
LEADER_REPLICATION_THROTTLED_REPLICAS("leader.replication.throttled.replicas"), LEADER_REPLICATION_THROTTLED_REPLICAS("leader.replication.throttled.replicas"),
MAX_COMPACTION_LAG_MS("max.compaction.lag.ms"), MAX_COMPACTION_LAG_MS("max.compaction.lag.ms"),
MESSAGE_DOWNCONVERSION_ENABLE("message.downconversion.enable"), MESSAGE_DOWNCONVERSION_ENABLE("message.downconversion.enable"),
MESSAGE_FORMAT_VERSION("message.format.version"), MESSAGE_FORMAT_VERSION("message.format.version"),
MESSAGE_TIMESTAMP_DIFFERENCE_MAX_MS("message.timestamp.difference.max.ms"), MESSAGE_TIMESTAMP_DIFFERENCE_MAX_MS("message.timestamp.difference.max.ms"),
MESSAGE_TIMESTAMP_TYPE("message.timestamp.type"), MESSAGE_TIMESTAMP_TYPE("message.timestamp.type"),
MIN_CLEANABLE_DIRTY_RATIO("min.cleanable.dirty.ratio"), MIN_CLEANABLE_DIRTY_RATIO("min.cleanable.dirty.ratio"),
MIN_COMPACTION_LAG_MS("min.compaction.lag.ms"), MIN_COMPACTION_LAG_MS("min.compaction.lag.ms"),
PREALLOCATE("preallocate"), PREALLOCATE("preallocate"),
RETENTION_BYTES("retention.bytes"), RETENTION_BYTES("retention.bytes"),
SEGMENT_BYTES("segment.bytes"), SEGMENT_BYTES("segment.bytes"),
SEGMENT_INDEX_BYTES("segment.index.bytes"), SEGMENT_INDEX_BYTES("segment.index.bytes"),
SEGMENT_JITTER_MS("segment.jitter.ms"), SEGMENT_JITTER_MS("segment.jitter.ms"),
SEGMENT_MS("segment.ms"), SEGMENT_MS("segment.ms"),
UNCLEAN_LEADER_ELECTION_ENABLE("unclean.leader.election.enable"); UNCLEAN_LEADER_ELECTION_ENABLE("unclean.leader.election.enable");
private final String optionValue; private final String optionValue;
CustomParameterType(String optionValue) { CustomParameterType(String optionValue) {
this.optionValue = optionValue; this.optionValue = optionValue;
} }
public String getOptionValue() { public String getOptionValue() {
return optionValue; return optionValue;
} }
} }

View file

@ -2,26 +2,26 @@ package com.provectus.kafka.ui.pages.topics.enums;
public enum MaxSizeOnDisk { public enum MaxSizeOnDisk {
NOT_SET("-1", "Not Set"), NOT_SET("-1", "Not Set"),
SIZE_1_GB("1073741824", "1 GB"), SIZE_1_GB("1073741824", "1 GB"),
SIZE_10_GB("10737418240", "10 GB"), SIZE_10_GB("10737418240", "10 GB"),
SIZE_20_GB("21474836480", "20 GB"), SIZE_20_GB("21474836480", "20 GB"),
SIZE_50_GB("53687091200", "50 GB"); SIZE_50_GB("53687091200", "50 GB");
private final String optionValue; private final String optionValue;
private final String visibleText; private final String visibleText;
MaxSizeOnDisk(String optionValue, String visibleText) { MaxSizeOnDisk(String optionValue, String visibleText) {
this.optionValue = optionValue; this.optionValue = optionValue;
this.visibleText = visibleText; this.visibleText = visibleText;
} }
public String getOptionValue() { public String getOptionValue() {
return optionValue; return optionValue;
} }
public String getVisibleText() { public String getVisibleText() {
return visibleText; return visibleText;
} }
} }

View file

@ -2,25 +2,25 @@ package com.provectus.kafka.ui.pages.topics.enums;
public enum TimeToRetain { public enum TimeToRetain {
BTN_12_HOURS("12 hours", "43200000"), BTN_12_HOURS("12 hours", "43200000"),
BTN_1_DAY("1 day", "86400000"), BTN_1_DAY("1 day", "86400000"),
BTN_2_DAYS("2 days", "172800000"), BTN_2_DAYS("2 days", "172800000"),
BTN_7_DAYS("7 days", "604800000"), BTN_7_DAYS("7 days", "604800000"),
BTN_4_WEEKS("4 weeks", "2419200000"); BTN_4_WEEKS("4 weeks", "2419200000");
private final String button; private final String button;
private final String value; private final String value;
TimeToRetain(String button, String value) { TimeToRetain(String button, String value) {
this.button = button; this.button = button;
this.value = value; this.value = value;
} }
public String getButton() { public String getButton() {
return button; return button;
} }
public String getValue() { public String getValue() {
return value; return value;
} }
} }

View file

@ -1,272 +1,282 @@
package com.provectus.kafka.ui.services; 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.fasterxml.jackson.databind.ObjectMapper;
import com.provectus.kafka.ui.api.ApiClient; import com.provectus.kafka.ui.api.ApiClient;
import com.provectus.kafka.ui.api.api.*; import com.provectus.kafka.ui.api.api.KafkaConnectApi;
import com.provectus.kafka.ui.api.model.*; import com.provectus.kafka.ui.api.api.KsqlApi;
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.KsqlCommandV2;
import com.provectus.kafka.ui.api.model.KsqlCommandV2Response;
import com.provectus.kafka.ui.api.model.KsqlResponse;
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.models.Connector; import com.provectus.kafka.ui.models.Connector;
import com.provectus.kafka.ui.models.Schema; import com.provectus.kafka.ui.models.Schema;
import com.provectus.kafka.ui.models.Topic; import com.provectus.kafka.ui.models.Topic;
import com.provectus.kafka.ui.pages.ksqlDb.models.Stream; import com.provectus.kafka.ui.pages.ksqldb.models.Stream;
import com.provectus.kafka.ui.pages.ksqlDb.models.Table; import com.provectus.kafka.ui.pages.ksqldb.models.Table;
import com.provectus.kafka.ui.settings.BaseSource; import com.provectus.kafka.ui.settings.BaseSource;
import io.qameta.allure.Step; import io.qameta.allure.Step;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import lombok.SneakyThrows;
import static com.codeborne.selenide.Selenide.sleep; import lombok.extern.slf4j.Slf4j;
import static com.provectus.kafka.ui.utilities.FileUtils.fileToString; import org.springframework.web.reactive.function.client.WebClientResponseException;
@Slf4j @Slf4j
public class ApiService extends BaseSource { public class ApiService extends BaseSource {
@SneakyThrows @SneakyThrows
private TopicsApi topicApi() { private TopicsApi topicApi() {
return new TopicsApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); return new TopicsApi(new ApiClient().setBasePath(BASE_API_URL));
} }
@SneakyThrows @SneakyThrows
private SchemasApi schemaApi() { private SchemasApi schemaApi() {
return new SchemasApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); return new SchemasApi(new ApiClient().setBasePath(BASE_API_URL));
} }
@SneakyThrows @SneakyThrows
private KafkaConnectApi connectorApi() { private KafkaConnectApi connectorApi() {
return new KafkaConnectApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); return new KafkaConnectApi(new ApiClient().setBasePath(BASE_API_URL));
} }
@SneakyThrows @SneakyThrows
private MessagesApi messageApi() { private MessagesApi messageApi() {
return new MessagesApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); return new MessagesApi(new ApiClient().setBasePath(BASE_API_URL));
} }
@SneakyThrows @SneakyThrows
private KsqlApi ksqlApi() { private KsqlApi ksqlApi() {
return new KsqlApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); return new KsqlApi(new ApiClient().setBasePath(BASE_API_URL));
} }
@SneakyThrows @SneakyThrows
private void createTopic(String clusterName, String topicName) { private void createTopic(String clusterName, String topicName) {
TopicCreation topic = new TopicCreation(); TopicCreation topic = new TopicCreation();
topic.setName(topicName); topic.setName(topicName);
topic.setPartitions(1); topic.setPartitions(1);
topic.setReplicationFactor(1); topic.setReplicationFactor(1);
try { try {
topicApi().createTopic(clusterName, topic).block(); topicApi().createTopic(clusterName, topic).block();
sleep(2000); sleep(2000);
} catch (WebClientResponseException ex) { } catch (WebClientResponseException ex) {
ex.printStackTrace(); ex.printStackTrace();
}
} }
}
@Step @Step
public ApiService createTopic(Topic topic) { public ApiService createTopic(Topic topic) {
createTopic(CLUSTER_NAME, topic.getName()); createTopic(CLUSTER_NAME, topic.getName());
return this; return this;
} }
@SneakyThrows @SneakyThrows
private void deleteTopic(String clusterName, String topicName) { private void deleteTopic(String clusterName, String topicName) {
try { try {
topicApi().deleteTopic(clusterName, topicName).block(); topicApi().deleteTopic(clusterName, topicName).block();
} catch (WebClientResponseException ignore) { } catch (WebClientResponseException ignored) {
}
} }
}
@Step @Step
public ApiService deleteTopic(String topicName) { public ApiService deleteTopic(String topicName) {
deleteTopic(CLUSTER_NAME, topicName); deleteTopic(CLUSTER_NAME, topicName);
return this; return this;
} }
@SneakyThrows @SneakyThrows
private void createSchema(String clusterName, Schema schema) { private void createSchema(String clusterName, Schema schema) {
NewSchemaSubject schemaSubject = new NewSchemaSubject(); NewSchemaSubject schemaSubject = new NewSchemaSubject();
schemaSubject.setSubject(schema.getName()); schemaSubject.setSubject(schema.getName());
schemaSubject.setSchema(fileToString(schema.getValuePath())); schemaSubject.setSchema(fileToString(schema.getValuePath()));
schemaSubject.setSchemaType(schema.getType()); schemaSubject.setSchemaType(schema.getType());
try { try {
schemaApi().createNewSchema(clusterName, schemaSubject).block(); schemaApi().createNewSchema(clusterName, schemaSubject).block();
} catch (WebClientResponseException ex) { } catch (WebClientResponseException ex) {
ex.printStackTrace(); ex.printStackTrace();
}
} }
}
@Step @Step
public ApiService createSchema(Schema schema) { public ApiService createSchema(Schema schema) {
createSchema(CLUSTER_NAME, schema); createSchema(CLUSTER_NAME, schema);
return this; return this;
} }
@SneakyThrows @SneakyThrows
private void deleteSchema(String clusterName, String schemaName) { private void deleteSchema(String clusterName, String schemaName) {
try { try {
schemaApi().deleteSchema(clusterName, schemaName).block(); schemaApi().deleteSchema(clusterName, schemaName).block();
} catch (WebClientResponseException ignore) { } catch (WebClientResponseException ignored) {
}
} }
}
@Step @Step
public ApiService deleteSchema(String schemaName) { public ApiService deleteSchema(String schemaName) {
deleteSchema(CLUSTER_NAME, schemaName); deleteSchema(CLUSTER_NAME, schemaName);
return this; return this;
} }
@SneakyThrows @SneakyThrows
private void deleteConnector(String clusterName, String connectName, String connectorName) { private void deleteConnector(String clusterName, String connectName, String connectorName) {
try { try {
connectorApi().deleteConnector(clusterName, connectName, connectorName).block(); connectorApi().deleteConnector(clusterName, connectName, connectorName).block();
} catch (WebClientResponseException ignore) { } catch (WebClientResponseException ignored) {
}
} }
}
@Step @Step
public ApiService deleteConnector(String connectName, String connectorName) { public ApiService deleteConnector(String connectName, String connectorName) {
deleteConnector(CLUSTER_NAME, connectName, connectorName); deleteConnector(CLUSTER_NAME, connectName, connectorName);
return this; return this;
} }
@Step @Step
public ApiService deleteConnector(String connectorName) { public ApiService deleteConnector(String connectorName) {
deleteConnector(CLUSTER_NAME, CONNECT_NAME, connectorName); deleteConnector(CLUSTER_NAME, CONNECT_NAME, connectorName);
return this; return this;
} }
@SneakyThrows @SneakyThrows
private void createConnector(String clusterName, String connectName, Connector connector) { private void createConnector(String clusterName, String connectName, Connector connector) {
NewConnector connectorProperties = new NewConnector(); NewConnector connectorProperties = new NewConnector();
connectorProperties.setName(connector.getName()); connectorProperties.setName(connector.getName());
Map<String, Object> configMap = new ObjectMapper().readValue(connector.getConfig(), HashMap.class); Map<String, Object> configMap = new ObjectMapper().readValue(connector.getConfig(), HashMap.class);
connectorProperties.setConfig(configMap); connectorProperties.setConfig(configMap);
try { try {
connectorApi().deleteConnector(clusterName, connectName, connector.getName()).block(); connectorApi().deleteConnector(clusterName, connectName, connector.getName()).block();
} catch (WebClientResponseException ignored) { } catch (WebClientResponseException ignored) {
}
connectorApi().createConnector(clusterName, connectName, connectorProperties).block();
} }
connectorApi().createConnector(clusterName, connectName, connectorProperties).block();
}
@Step @Step
public ApiService createConnector(String connectName, Connector connector) { public ApiService createConnector(String connectName, Connector connector) {
createConnector(CLUSTER_NAME, connectName, connector); createConnector(CLUSTER_NAME, connectName, connector);
return this; return this;
} }
@Step @Step
public ApiService createConnector(Connector connector) { public ApiService createConnector(Connector connector) {
createConnector(CLUSTER_NAME, CONNECT_NAME, connector); createConnector(CLUSTER_NAME, CONNECT_NAME, connector);
return this; return this;
} }
@Step @Step
public String getFirstConnectName(String clusterName) { public String getFirstConnectName(String clusterName) {
return Objects.requireNonNull(connectorApi().getConnects(clusterName).blockFirst()).getName(); return Objects.requireNonNull(connectorApi().getConnects(clusterName).blockFirst()).getName();
} }
@SneakyThrows @SneakyThrows
private void sendMessage(String clusterName, Topic topic) { private void sendMessage(String clusterName, Topic topic) {
CreateTopicMessage createMessage = new CreateTopicMessage(); CreateTopicMessage createMessage = new CreateTopicMessage();
createMessage.setPartition(0); createMessage.setPartition(0);
createMessage.setKeySerde("String"); createMessage.setKeySerde("String");
createMessage.setValueSerde("String"); createMessage.setValueSerde("String");
createMessage.setKey(topic.getMessageKey()); createMessage.setKey(topic.getMessageKey());
createMessage.setContent(topic.getMessageContent()); createMessage.setContent(topic.getMessageValue());
try { try {
messageApi().sendTopicMessages(clusterName, topic.getName(), createMessage).block(); messageApi().sendTopicMessages(clusterName, topic.getName(), createMessage).block();
} catch (WebClientResponseException ex) { } catch (WebClientResponseException ex) {
ex.getRawStatusCode(); ex.getRawStatusCode();
}
} }
}
@Step @Step
public ApiService sendMessage(Topic topic) { public ApiService sendMessage(Topic topic) {
sendMessage(CLUSTER_NAME, topic); sendMessage(CLUSTER_NAME, topic);
return this; return this;
} }
@Step @Step
public ApiService createStream(Stream stream) { public ApiService createStream(Stream stream) {
KsqlCommandV2Response pipeIdStream = ksqlApi() KsqlCommandV2Response pipeIdStream = ksqlApi()
.executeKsql(CLUSTER_NAME, new KsqlCommandV2() .executeKsql(CLUSTER_NAME, new KsqlCommandV2()
.ksql(String.format("CREATE STREAM %s (profileId VARCHAR, latitude DOUBLE, longitude DOUBLE) ", .ksql(String.format("CREATE STREAM %s (profileId VARCHAR, latitude DOUBLE, longitude DOUBLE) ",
stream.getName()) stream.getName())
+ String.format("WITH (kafka_topic='%s', value_format='json', partitions=1);", + String.format("WITH (kafka_topic='%s', value_format='json', partitions=1);",
stream.getTopicName()))) stream.getTopicName())))
.block(); .block();
assert pipeIdStream != null; assert pipeIdStream != null;
List<KsqlResponse> responseListStream = ksqlApi() List<KsqlResponse> responseListStream = ksqlApi()
.openKsqlResponsePipe(CLUSTER_NAME, pipeIdStream.getPipeId()) .openKsqlResponsePipe(CLUSTER_NAME, pipeIdStream.getPipeId())
.collectList() .collectList()
.block(); .block();
assert Objects.requireNonNull(responseListStream).size() != 0; assert Objects.requireNonNull(responseListStream).size() != 0;
return this; return this;
} }
@Step @Step
public ApiService createTables(Table firstTable, Table secondTable) { public ApiService createTables(Table firstTable, Table secondTable) {
KsqlCommandV2Response pipeIdTable1 = ksqlApi() KsqlCommandV2Response pipeIdTable1 = ksqlApi()
.executeKsql(CLUSTER_NAME, new KsqlCommandV2() .executeKsql(CLUSTER_NAME, new KsqlCommandV2()
.ksql(String.format("CREATE TABLE %s AS ", firstTable.getName()) .ksql(String.format("CREATE TABLE %s AS ", firstTable.getName())
+ " SELECT profileId, " + " SELECT profileId, "
+ " LATEST_BY_OFFSET(latitude) AS la, " + " LATEST_BY_OFFSET(latitude) AS la, "
+ " LATEST_BY_OFFSET(longitude) AS lo " + " LATEST_BY_OFFSET(longitude) AS lo "
+ String.format(" FROM %s ", firstTable.getStreamName()) + String.format(" FROM %s ", firstTable.getStreamName())
+ " GROUP BY profileId " + " GROUP BY profileId "
+ " EMIT CHANGES;")) + " EMIT CHANGES;"))
.block(); .block();
assert pipeIdTable1 != null; assert pipeIdTable1 != null;
List<KsqlResponse> responseListTable = ksqlApi() List<KsqlResponse> responseListTable = ksqlApi()
.openKsqlResponsePipe(CLUSTER_NAME, pipeIdTable1.getPipeId()) .openKsqlResponsePipe(CLUSTER_NAME, pipeIdTable1.getPipeId())
.collectList() .collectList()
.block(); .block();
assert Objects.requireNonNull(responseListTable).size() != 0; assert Objects.requireNonNull(responseListTable).size() != 0;
KsqlCommandV2Response pipeIdTable2 = ksqlApi() KsqlCommandV2Response pipeIdTable2 = ksqlApi()
.executeKsql(CLUSTER_NAME, new KsqlCommandV2() .executeKsql(CLUSTER_NAME, new KsqlCommandV2()
.ksql(String.format("CREATE TABLE %s AS ", secondTable.getName()) .ksql(String.format("CREATE TABLE %s AS ", secondTable.getName())
+ " SELECT ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1) AS distanceInMiles, " + " SELECT ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1) AS distanceInMiles, "
+ " COLLECT_LIST(profileId) AS riders, " + " COLLECT_LIST(profileId) AS riders, "
+ " COUNT(*) AS count " + " COUNT(*) AS count "
+ String.format(" FROM %s ", firstTable.getName()) + String.format(" FROM %s ", firstTable.getName())
+ " GROUP BY ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1);")) + " GROUP BY ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1);"))
.block(); .block();
assert pipeIdTable2 != null; assert pipeIdTable2 != null;
List<KsqlResponse> responseListTable2 = ksqlApi() List<KsqlResponse> responseListTable2 = ksqlApi()
.openKsqlResponsePipe(CLUSTER_NAME, pipeIdTable2.getPipeId()) .openKsqlResponsePipe(CLUSTER_NAME, pipeIdTable2.getPipeId())
.collectList() .collectList()
.block(); .block();
assert Objects.requireNonNull(responseListTable2).size() != 0; assert Objects.requireNonNull(responseListTable2).size() != 0;
return this; return this;
} }
@Step @Step
public ApiService insertInto(Stream stream) { public ApiService insertInto(Stream stream) {
String streamName = stream.getName(); String streamName = stream.getName();
KsqlCommandV2Response pipeIdInsert = ksqlApi() KsqlCommandV2Response pipeIdInsert = ksqlApi()
.executeKsql(CLUSTER_NAME, new KsqlCommandV2() .executeKsql(CLUSTER_NAME, new KsqlCommandV2()
.ksql("INSERT INTO " + streamName + " (profileId, latitude, longitude) VALUES ('c2309eec', 37.7877, -122.4205);" .ksql("INSERT INTO " + streamName
+ "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('c2309eec', 37.7877, -122.4205);"
" (profileId, latitude, longitude) VALUES ('18f4ea86', 37.3903, -122.0643); " + "INSERT INTO " + streamName
+ "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('18f4ea86', 37.3903, -122.0643); "
" (profileId, latitude, longitude) VALUES ('4ab5cbad', 37.3952, -122.0813); " + "INSERT INTO " + streamName
+ "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('4ab5cbad', 37.3952, -122.0813); "
" (profileId, latitude, longitude) VALUES ('8b6eae59', 37.3944, -122.0813); " + "INSERT INTO " + streamName
+ "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('8b6eae59', 37.3944, -122.0813); "
" (profileId, latitude, longitude) VALUES ('4a7c7b41', 37.4049, -122.0822); " + "INSERT INTO " + streamName
+ "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('4a7c7b41', 37.4049, -122.0822); "
" (profileId, latitude, longitude) VALUES ('4ddad000', 37.7857, -122.4011);")) + "INSERT INTO " + streamName
.block(); + " (profileId, latitude, longitude) VALUES ('4ddad000', 37.7857, -122.4011);"))
assert pipeIdInsert != null; .block();
List<KsqlResponse> responseListInsert = ksqlApi() assert pipeIdInsert != null;
.openKsqlResponsePipe(CLUSTER_NAME, pipeIdInsert.getPipeId()) List<KsqlResponse> responseListInsert = ksqlApi()
.collectList() .openKsqlResponsePipe(CLUSTER_NAME, pipeIdInsert.getPipeId())
.block(); .collectList()
assert Objects.requireNonNull(responseListInsert).size() != 0; .block();
return this; assert Objects.requireNonNull(responseListInsert).size() != 0;
} return this;
}
} }

View file

@ -1,22 +1,29 @@
package com.provectus.kafka.ui.settings; package com.provectus.kafka.ui.settings;
import static com.provectus.kafka.ui.variables.Browser.LOCAL;
import com.provectus.kafka.ui.settings.configs.Config; import com.provectus.kafka.ui.settings.configs.Config;
import org.aeonbits.owner.ConfigFactory; import org.aeonbits.owner.ConfigFactory;
public abstract class BaseSource { public abstract class BaseSource {
public static final String BASE_CONTAINER_URL = "http://host.testcontainers.internal:8080"; public static final String CLUSTER_NAME = "local";
public static final String BASE_LOCAL_URL = "http://localhost:8080"; public static final String CONNECT_NAME = "first";
public static final String CLUSTER_NAME = "local"; private static final String LOCAL_HOST = "localhost";
public static final String CONNECT_NAME = "first"; public static final String REMOTE_URL = String.format("http://%s:4444/wd/hub", LOCAL_HOST);
private static Config config; public static final String BASE_API_URL = String.format("http://%s:8080", LOCAL_HOST);
public static final String BROWSER = config().browser(); private static Config config;
public static final String SUITE_NAME = config().suite(); public static final String BROWSER = config().browser();
public static final String BASE_HOST = BROWSER.equals(LOCAL)
? LOCAL_HOST
: "host.docker.internal";
public static final String BASE_UI_URL = String.format("http://%s:8080", BASE_HOST);
public static final String SUITE_NAME = config().suite();
private static Config config() { private static Config config() {
if (config == null) { if (config == null) {
config = ConfigFactory.create(Config.class, System.getProperties()); config = ConfigFactory.create(Config.class, System.getProperties());
}
return config;
} }
return config;
}
} }

View file

@ -1,17 +1,17 @@
package com.provectus.kafka.ui.settings.configs; 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.Browser.CONTAINER;
import static com.provectus.kafka.ui.variables.Suite.CUSTOM; import static com.provectus.kafka.ui.variables.Suite.CUSTOM;
import org.aeonbits.owner.Config;
public interface Profiles extends Config { public interface Profiles extends Config {
@Key("browser") @Key("browser")
@DefaultValue(CONTAINER) @DefaultValue(CONTAINER)
String browser(); String browser();
@Key("suite") @Key("suite")
@DefaultValue(CUSTOM) @DefaultValue(CUSTOM)
String suite(); String suite();
} }

View file

@ -1,68 +0,0 @@
package com.provectus.kafka.ui.settings.drivers;
import static com.codeborne.selenide.Selenide.clearBrowserCookies;
import static com.codeborne.selenide.Selenide.clearBrowserLocalStorage;
import static com.codeborne.selenide.Selenide.open;
import static com.codeborne.selenide.Selenide.refresh;
import com.codeborne.selenide.Configuration;
import com.codeborne.selenide.WebDriverRunner;
import com.codeborne.selenide.logevents.SelenideLogger;
import io.qameta.allure.Step;
import io.qameta.allure.selenide.AllureSelenide;
import org.openqa.selenium.chrome.ChromeOptions;
public abstract class LocalWebDriver {
private static org.openqa.selenium.WebDriver getWebDriver() {
try {
return WebDriverRunner.getWebDriver();
} catch (IllegalStateException ex) {
Configuration.headless = false;
Configuration.browser = "chrome";
Configuration.browserSize = "1920x1080";
/**screenshots and savePageSource config is needed for local debug
* optionally can be set as 'false' to not duplicate Allure report
*/
Configuration.screenshots = true;
Configuration.savePageSource = true;
Configuration.pageLoadTimeout = 120000;
Configuration.browserCapabilities = new ChromeOptions()
.addArguments("--remote-allow-origins=*")
.addArguments("--lang=en_US");
open();
return WebDriverRunner.getWebDriver();
}
}
@Step
public static void openUrl(String url) {
if (!getWebDriver().getCurrentUrl().equals(url)) {
getWebDriver().get(url);
}
}
@Step
public static void browserInit() {
getWebDriver();
}
@Step
public static void browserClear() {
clearBrowserLocalStorage();
clearBrowserCookies();
refresh();
}
@Step
public static void browserQuit() {
getWebDriver().quit();
}
@Step
public static void loggerSetup() {
SelenideLogger.addListener("AllureSelenide", new AllureSelenide()
.screenshots(true)
.savePageSource(false));
}
}

View file

@ -0,0 +1,101 @@
package com.provectus.kafka.ui.settings.drivers;
import static com.codeborne.selenide.Selenide.clearBrowserCookies;
import static com.codeborne.selenide.Selenide.clearBrowserLocalStorage;
import static com.codeborne.selenide.Selenide.refresh;
import static com.provectus.kafka.ui.settings.BaseSource.BROWSER;
import static com.provectus.kafka.ui.settings.BaseSource.REMOTE_URL;
import static com.provectus.kafka.ui.variables.Browser.CONTAINER;
import static com.provectus.kafka.ui.variables.Browser.LOCAL;
import com.codeborne.selenide.Configuration;
import com.codeborne.selenide.Selenide;
import com.codeborne.selenide.WebDriverRunner;
import com.codeborne.selenide.logevents.SelenideLogger;
import io.qameta.allure.Step;
import io.qameta.allure.selenide.AllureSelenide;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.DesiredCapabilities;
@Slf4j
public abstract class WebDriver {
@Step
public static void browserSetup() {
Configuration.headless = false;
Configuration.browser = "chrome";
Configuration.browserSize = "1920x1080";
Configuration.screenshots = true;
Configuration.savePageSource = false;
Configuration.pageLoadTimeout = 120000;
ChromeOptions options = new ChromeOptions()
.addArguments("--no-sandbox")
.addArguments("--verbose")
.addArguments("--remote-allow-origins=*")
.addArguments("--disable-dev-shm-usage")
.addArguments("--disable-gpu")
.addArguments("--lang=en_US");
switch (BROWSER) {
case (LOCAL) -> Configuration.browserCapabilities = options;
case (CONTAINER) -> {
Configuration.remote = REMOTE_URL;
Configuration.remoteConnectionTimeout = 180000;
DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("enableVNC", true);
capabilities.setCapability("enableVideo", false);
Configuration.browserCapabilities = capabilities.merge(options);
}
default -> throw new IllegalStateException("Unexpected value: " + BROWSER);
}
}
private static org.openqa.selenium.WebDriver getWebDriver() {
try {
return WebDriverRunner.getWebDriver();
} catch (IllegalStateException ex) {
browserSetup();
Selenide.open();
return WebDriverRunner.getWebDriver();
}
}
@Step
public static void openUrl(String url) {
org.openqa.selenium.WebDriver driver = getWebDriver();
if (!driver.getCurrentUrl().equals(url)) {
driver.get(url);
}
}
@Step
public static void browserInit() {
getWebDriver();
}
@Step
public static void browserClear() {
clearBrowserLocalStorage();
clearBrowserCookies();
refresh();
}
@Step
public static void browserQuit() {
org.openqa.selenium.WebDriver driver = null;
try {
driver = WebDriverRunner.getWebDriver();
} catch (Throwable ignored) {
}
if (driver != null) {
driver.quit();
}
}
@Step
public static void loggerSetup() {
SelenideLogger.addListener("AllureSelenide", new AllureSelenide()
.screenshots(true)
.savePageSource(false));
}
}

View file

@ -1,35 +1,39 @@
package com.provectus.kafka.ui.settings.listeners; package com.provectus.kafka.ui.settings.listeners;
import static java.nio.file.Files.newInputStream;
import com.codeborne.selenide.Screenshots; import com.codeborne.selenide.Screenshots;
import io.qameta.allure.Allure; import io.qameta.allure.Allure;
import io.qameta.allure.testng.AllureTestNg; import io.qameta.allure.testng.AllureTestNg;
import java.io.File;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.testng.ITestListener; import org.testng.ITestListener;
import org.testng.ITestResult; import org.testng.ITestResult;
import java.io.File; @Slf4j
import java.io.IOException;
import java.util.Objects;
import static java.nio.file.Files.newInputStream;
public class AllureListener extends AllureTestNg implements ITestListener { public class AllureListener extends AllureTestNg implements ITestListener {
private void takeScreenshot() { private void takeScreenshot() {
File screenshot = Screenshots.takeScreenShotAsFile(); File screenshot = Screenshots.takeScreenShotAsFile();
try { try {
Allure.addAttachment(Objects.requireNonNull(screenshot).getName(), newInputStream(screenshot.toPath())); if (screenshot != null) {
} catch (IOException e) { Allure.addAttachment(screenshot.getName(), newInputStream(screenshot.toPath()));
throw new RuntimeException(e); } else {
} log.warn("Unable to take screenshot");
}
} catch (IOException e) {
throw new RuntimeException(e);
} }
}
@Override @Override
public void onTestFailure(ITestResult result) { public void onTestFailure(ITestResult result) {
takeScreenshot(); takeScreenshot();
} }
@Override @Override
public void onTestSkipped(ITestResult result) { public void onTestSkipped(ITestResult result) {
takeScreenshot(); takeScreenshot();
} }
} }

View file

@ -7,31 +7,31 @@ import org.testng.TestListenerAdapter;
@Slf4j @Slf4j
public class LoggerListener extends TestListenerAdapter { public class LoggerListener extends TestListenerAdapter {
@Override @Override
public void onTestStart(final ITestResult testResult) { public void onTestStart(final ITestResult testResult) {
log.info(String.format("\n------------------------------------------------------------------------ " + log.info(String.format("\n------------------------------------------------------------------------ "
"\nTEST STARTED: %s.%s \n------------------------------------------------------------------------ \n", + "\nTEST STARTED: %s.%s \n------------------------------------------------------------------------ \n",
testResult.getInstanceName(), testResult.getName())); testResult.getInstanceName(), testResult.getName()));
} }
@Override @Override
public void onTestSuccess(final ITestResult testResult) { public void onTestSuccess(final ITestResult testResult) {
log.info(String.format("\n------------------------------------------------------------------------ " + log.info(String.format("\n------------------------------------------------------------------------ "
"\nTEST PASSED: %s.%s \n------------------------------------------------------------------------ \n", + "\nTEST PASSED: %s.%s \n------------------------------------------------------------------------ \n",
testResult.getInstanceName(), testResult.getName())); testResult.getInstanceName(), testResult.getName()));
} }
@Override @Override
public void onTestFailure(final ITestResult testResult) { public void onTestFailure(final ITestResult testResult) {
log.info(String.format("\n------------------------------------------------------------------------ " + log.info(String.format("\n------------------------------------------------------------------------ "
"\nTEST FAILED: %s.%s \n------------------------------------------------------------------------ \n", + "\nTEST FAILED: %s.%s \n------------------------------------------------------------------------ \n",
testResult.getInstanceName(), testResult.getName())); testResult.getInstanceName(), testResult.getName()));
} }
@Override @Override
public void onTestSkipped(final ITestResult testResult) { public void onTestSkipped(final ITestResult testResult) {
log.info(String.format("\n------------------------------------------------------------------------ " + log.info(String.format("\n------------------------------------------------------------------------ "
"\nTEST SKIPPED: %s.%s \n------------------------------------------------------------------------ \n", + "\nTEST SKIPPED: %s.%s \n------------------------------------------------------------------------ \n",
testResult.getInstanceName(), testResult.getName())); testResult.getInstanceName(), testResult.getName()));
} }
} }

View file

@ -1,14 +1,28 @@
package com.provectus.kafka.ui.settings.listeners; package com.provectus.kafka.ui.settings.listeners;
import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; import static io.qase.api.utils.IntegrationUtils.getCaseTitle;
import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Status;
import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Suite; import com.provectus.kafka.ui.utilities.qase.annotations.Automation;
import com.provectus.kafka.ui.utilities.qase.annotations.Status;
import com.provectus.kafka.ui.utilities.qase.annotations.Suite;
import io.qase.api.QaseClient; import io.qase.api.QaseClient;
import io.qase.api.StepStorage; import io.qase.api.StepStorage;
import io.qase.api.annotation.QaseId; import io.qase.api.annotation.QaseId;
import io.qase.client.ApiClient; import io.qase.client.ApiClient;
import io.qase.client.api.CasesApi; import io.qase.client.api.CasesApi;
import io.qase.client.model.*; import io.qase.client.model.GetCasesFiltersParameter;
import io.qase.client.model.ResultCreateStepsInner;
import io.qase.client.model.TestCase;
import io.qase.client.model.TestCaseCreate;
import io.qase.client.model.TestCaseCreateStepsInner;
import io.qase.client.model.TestCaseListResponse;
import io.qase.client.model.TestCaseListResponseAllOfResult;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.testng.Assert; import org.testng.Assert;
@ -16,108 +30,107 @@ import org.testng.ITestListener;
import org.testng.ITestResult; import org.testng.ITestResult;
import org.testng.TestListenerAdapter; import org.testng.TestListenerAdapter;
import java.lang.reflect.Method;
import java.util.*;
import static io.qase.api.utils.IntegrationUtils.getCaseTitle;
@Slf4j @Slf4j
public class QaseCreateListener extends TestListenerAdapter implements ITestListener { public class QaseCreateListener extends TestListenerAdapter implements ITestListener {
private static final CasesApi QASE_API = getQaseApi(); private static final CasesApi QASE_API = getQaseApi();
private static CasesApi getQaseApi() { private static CasesApi getQaseApi() {
ApiClient apiClient = QaseClient.getApiClient(); ApiClient apiClient = QaseClient.getApiClient();
apiClient.setApiKey(System.getProperty("QASEIO_API_TOKEN")); apiClient.setApiKey(System.getProperty("QASEIO_API_TOKEN"));
return new CasesApi(apiClient); 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 getStatus(Method method) { private static int getAutomation(Method method) {
if (method.isAnnotationPresent(Status.class)) if (method.isAnnotationPresent(Automation.class)) {
return method.getDeclaredAnnotation(Status.class).status().getValue(); return method.getDeclaredAnnotation(Automation.class).state().getValue();
return 1;
} }
return 0;
}
private static int getAutomation(Method method) { @SneakyThrows
if (method.isAnnotationPresent(Automation.class)) private static HashMap<Long, String> getCaseTitlesAndIdsFromQase() {
return method.getDeclaredAnnotation(Automation.class).state().getValue(); HashMap<Long, String> cases = new HashMap<>();
return 0; boolean getCases = true;
} int offSet = 0;
while (getCases) {
@SneakyThrows getCases = false;
private static HashMap<Long, String> getCaseTitlesAndIdsFromQase() { TestCaseListResponse response = QASE_API.getCases(System.getProperty("QASE_PROJECT_CODE"),
HashMap<Long, String> cases = new HashMap<>(); new GetCasesFiltersParameter().status(GetCasesFiltersParameter.SERIALIZED_NAME_STATUS), 100, offSet);
boolean getCases = true; TestCaseListResponseAllOfResult result = response.getResult();
int offSet = 0; Assert.assertNotNull(result);
while (getCases) { List<TestCase> entities = result.getEntities();
getCases = false; Assert.assertNotNull(entities);
TestCaseListResponse response = QASE_API.getCases(System.getProperty("QASE_PROJECT_CODE"), if (entities.size() > 0) {
new GetCasesFiltersParameter().status(GetCasesFiltersParameter.SERIALIZED_NAME_STATUS), 100, offSet); for (TestCase testCase : entities) {
TestCaseListResponseAllOfResult result = response.getResult(); cases.put(testCase.getId(), testCase.getTitle());
Assert.assertNotNull(result);
List<TestCase> 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; offSet = offSet + 100;
getCases = true;
}
} }
return cases;
}
private static boolean isCaseWithTitleExistInQase(Method method) { private static boolean isCaseWithTitleExistInQase(Method method) {
HashMap<Long, String> cases = getCaseTitlesAndIdsFromQase(); HashMap<Long, String> cases = getCaseTitlesAndIdsFromQase();
String title = getCaseTitle(method); String title = getCaseTitle(method);
if (cases.containsValue(title)) { if (cases.containsValue(title)) {
for (Map.Entry<Long, String> map : cases.entrySet()) { for (Map.Entry<Long, String> map : cases.entrySet()) {
if (map.getValue().matches(title)) { if (map.getValue().matches(title)) {
long id = map.getKey(); long id = map.getKey();
log.warn(String.format("Test case with @QaseTitle='%s' already exists with @QaseId=%d. " + log.warn(String.format("Test case with @QaseTitle='%s' already exists with @QaseId=%d. "
"Please verify @QaseTitle annotation", title, id)); + "Please verify @QaseTitle annotation", title, id));
return true; return true;
}
}
} }
return false; }
} }
return false;
}
@Override @Override
@SneakyThrows @SneakyThrows
public void onTestSuccess(final ITestResult testResult) { public void onTestSuccess(final ITestResult testResult) {
Method method = testResult.getMethod() Method method = testResult.getMethod()
.getConstructorOrMethod() .getConstructorOrMethod()
.getMethod(); .getMethod();
String title = getCaseTitle(method); String title = getCaseTitle(method);
if (!method.isAnnotationPresent(QaseId.class)) { if (!method.isAnnotationPresent(QaseId.class)) {
if (title != null) { if (title != null) {
if (!isCaseWithTitleExistInQase(method)) { if (!isCaseWithTitleExistInQase(method)) {
LinkedList<ResultCreateStepsInner> resultSteps = StepStorage.stopSteps(); LinkedList<ResultCreateStepsInner> resultSteps = StepStorage.stopSteps();
LinkedList<TestCaseCreateStepsInner> createSteps = new LinkedList<>(); LinkedList<TestCaseCreateStepsInner> createSteps = new LinkedList<>();
resultSteps.forEach(step -> { resultSteps.forEach(step -> {
TestCaseCreateStepsInner caseStep = new TestCaseCreateStepsInner(); TestCaseCreateStepsInner caseStep = new TestCaseCreateStepsInner();
caseStep.setAction(step.getAction()); caseStep.setAction(step.getAction());
caseStep.setExpectedResult(step.getExpectedResult()); caseStep.setExpectedResult(step.getExpectedResult());
createSteps.add(caseStep); createSteps.add(caseStep);
}); });
TestCaseCreate newCase = new TestCaseCreate(); TestCaseCreate newCase = new TestCaseCreate();
newCase.setTitle(title); newCase.setTitle(title);
newCase.setStatus(getStatus(method)); newCase.setStatus(getStatus(method));
newCase.setAutomation(getAutomation(method)); newCase.setAutomation(getAutomation(method));
newCase.setSteps(createSteps); newCase.setSteps(createSteps);
if (method.isAnnotationPresent(Suite.class)) { if (method.isAnnotationPresent(Suite.class)) {
long suiteId = method.getDeclaredAnnotation(Suite.class).id(); long suiteId = method.getDeclaredAnnotation(Suite.class).id();
newCase.suiteId(suiteId); newCase.suiteId(suiteId);
} }
Long id = Objects.requireNonNull(QASE_API.createCase(System.getProperty("QASE_PROJECT_CODE"), Long id = Objects.requireNonNull(QASE_API.createCase(System.getProperty("QASE_PROJECT_CODE"),
newCase).getResult()).getId(); newCase).getResult()).getId();
log.info(String.format("New test case '%s' was created with @QaseId=%d", title, id)); log.info(String.format("New test case '%s' was created with @QaseId=%d", title, id));
} }
} else } else {
log.warn("To create new test case in Qase.io please add @QaseTitle annotation"); 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"); } else {
log.warn("To create new test case in Qase.io please remove @QaseId annotation");
} }
}
} }

View file

@ -1,5 +1,12 @@
package com.provectus.kafka.ui.settings.listeners; package com.provectus.kafka.ui.settings.listeners;
import static io.qase.api.utils.IntegrationUtils.getCaseId;
import static io.qase.api.utils.IntegrationUtils.getCaseTitle;
import static io.qase.api.utils.IntegrationUtils.getStacktrace;
import static io.qase.client.model.ResultCreate.StatusEnum.FAILED;
import static io.qase.client.model.ResultCreate.StatusEnum.PASSED;
import static io.qase.client.model.ResultCreate.StatusEnum.SKIPPED;
import io.qase.api.StepStorage; import io.qase.api.StepStorage;
import io.qase.api.config.QaseConfig; import io.qase.api.config.QaseConfig;
import io.qase.api.services.QaseTestCaseListener; import io.qase.api.services.QaseTestCaseListener;
@ -7,6 +14,9 @@ import io.qase.client.model.ResultCreate;
import io.qase.client.model.ResultCreateCase; import io.qase.client.model.ResultCreateCase;
import io.qase.client.model.ResultCreateStepsInner; import io.qase.client.model.ResultCreateStepsInner;
import io.qase.testng.guice.module.TestNgModule; import io.qase.testng.guice.module.TestNgModule;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.Optional;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -15,88 +25,81 @@ import org.testng.ITestListener;
import org.testng.ITestResult; import org.testng.ITestResult;
import org.testng.TestListenerAdapter; 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 @Slf4j
public class QaseResultListener extends TestListenerAdapter implements ITestListener { public class QaseResultListener extends TestListenerAdapter implements ITestListener {
private static final String REPORTER_NAME = "TestNG"; private static final String REPORTER_NAME = "TestNG";
static { static {
System.setProperty(QaseConfig.QASE_CLIENT_REPORTER_NAME_KEY, REPORTER_NAME); System.setProperty(QaseConfig.QASE_CLIENT_REPORTER_NAME_KEY, REPORTER_NAME);
} }
@Getter(lazy = true, value = AccessLevel.PRIVATE) @Getter(lazy = true, value = AccessLevel.PRIVATE)
private final QaseTestCaseListener qaseTestCaseListener = createQaseListener(); private final QaseTestCaseListener qaseTestCaseListener = createQaseListener();
private static QaseTestCaseListener createQaseListener() { private static QaseTestCaseListener createQaseListener() {
return TestNgModule.getInjector().getInstance(QaseTestCaseListener.class); return TestNgModule.getInjector().getInstance(QaseTestCaseListener.class);
} }
@Override @Override
public void onTestStart(ITestResult tr) { public void onTestStart(ITestResult tr) {
getQaseTestCaseListener().onTestCaseStarted(); getQaseTestCaseListener().onTestCaseStarted();
super.onTestStart(tr); super.onTestStart(tr);
} }
@Override @Override
public void onTestSuccess(ITestResult tr) { public void onTestSuccess(ITestResult tr) {
getQaseTestCaseListener() getQaseTestCaseListener()
.onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, PASSED)); .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, PASSED));
super.onTestSuccess(tr); super.onTestSuccess(tr);
} }
@Override @Override
public void onTestSkipped(ITestResult tr) { public void onTestSkipped(ITestResult tr) {
getQaseTestCaseListener() getQaseTestCaseListener()
.onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, SKIPPED)); .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, SKIPPED));
super.onTestSuccess(tr); super.onTestSuccess(tr);
} }
@Override @Override
public void onTestFailure(ITestResult tr) { public void onTestFailure(ITestResult tr) {
getQaseTestCaseListener() getQaseTestCaseListener()
.onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, FAILED)); .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, FAILED));
super.onTestFailure(tr); super.onTestFailure(tr);
} }
@Override @Override
public void onFinish(ITestContext testContext) { public void onFinish(ITestContext testContext) {
getQaseTestCaseListener().onTestCasesSetFinished(); getQaseTestCaseListener().onTestCasesSetFinished();
super.onFinish(testContext); super.onFinish(testContext);
} }
private void setupResultItem(ResultCreate resultCreate, ITestResult result, ResultCreate.StatusEnum status) { private void setupResultItem(ResultCreate resultCreate, ITestResult result, ResultCreate.StatusEnum status) {
Optional<Throwable> resultThrowable = Optional.ofNullable(result.getThrowable()); Optional<Throwable> resultThrowable = Optional.ofNullable(result.getThrowable());
String comment = resultThrowable String comment = resultThrowable
.flatMap(throwable -> Optional.of(throwable.toString())).orElse(null); .flatMap(throwable -> Optional.of(throwable.toString())).orElse(null);
Boolean isDefect = resultThrowable Boolean isDefect = resultThrowable
.flatMap(throwable -> Optional.of(throwable instanceof AssertionError)) .flatMap(throwable -> Optional.of(throwable instanceof AssertionError))
.orElse(false); .orElse(false);
String stacktrace = resultThrowable String stacktrace = resultThrowable
.flatMap(throwable -> Optional.of(getStacktrace(throwable))) .flatMap(throwable -> Optional.of(getStacktrace(throwable)))
.orElse(null); .orElse(null);
Method method = result.getMethod() Method method = result.getMethod()
.getConstructorOrMethod() .getConstructorOrMethod()
.getMethod(); .getMethod();
Long caseId = getCaseId(method); Long caseId = getCaseId(method);
String caseTitle = null; String caseTitle = null;
if (caseId == null) { if (caseId == null) {
caseTitle = getCaseTitle(method); caseTitle = getCaseTitle(method);
}
LinkedList<ResultCreateStepsInner> 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);
} }
LinkedList<ResultCreateStepsInner> 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);
}
} }

View file

@ -1,27 +1,26 @@
package com.provectus.kafka.ui.utilities; package com.provectus.kafka.ui.utilities;
import org.testcontainers.shaded.org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static org.apache.kafka.common.utils.Utils.readFileAsString; import static org.apache.kafka.common.utils.Utils.readFileAsString;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.testcontainers.shaded.org.apache.commons.io.IOUtils;
public class FileUtils { public class FileUtils {
public static String getResourceAsString(String resourceFileName) { public static String getResourceAsString(String resourceFileName) {
try { try {
return IOUtils.resourceToString("/" + resourceFileName, StandardCharsets.UTF_8); return IOUtils.resourceToString("/" + resourceFileName, StandardCharsets.UTF_8);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
}
} }
}
public static String fileToString(String path) { public static String fileToString(String path) {
try { try {
return readFileAsString(path); return readFileAsString(path);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
}
} }
}
} }

View file

@ -1,17 +1,16 @@
package com.provectus.kafka.ui.utilities; package com.provectus.kafka.ui.utilities;
import lombok.extern.slf4j.Slf4j; import static com.codeborne.selenide.Selenide.sleep;
import java.time.LocalTime; import java.time.LocalTime;
import lombok.extern.slf4j.Slf4j;
import static com.codeborne.selenide.Selenide.sleep;
@Slf4j @Slf4j
public class TimeUtils { public class TimeUtils {
public static void waitUntilNewMinuteStarted() { public static void waitUntilNewMinuteStarted() {
int secondsLeft = 60 - LocalTime.now().getSecond(); int secondsLeft = 60 - LocalTime.now().getSecond();
log.debug("\nwaitUntilNewMinuteStarted: {}s", secondsLeft); log.debug("\nwaitUntilNewMinuteStarted: {}s", secondsLeft);
sleep(secondsLeft * 1000); sleep(secondsLeft * 1000);
} }
} }

View file

@ -1,90 +1,110 @@
package com.provectus.kafka.ui.utilities; package com.provectus.kafka.ui.utilities;
import static com.codeborne.selenide.Selenide.executeJavaScript;
import com.codeborne.selenide.Condition; import com.codeborne.selenide.Condition;
import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.SelenideElement;
import com.codeborne.selenide.WebDriverRunner; import com.codeborne.selenide.WebDriverRunner;
import java.time.Duration;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.Keys; import org.openqa.selenium.Keys;
import org.openqa.selenium.interactions.Actions; import org.openqa.selenium.interactions.Actions;
import static com.codeborne.selenide.Selenide.executeJavaScript;
@Slf4j @Slf4j
public class WebUtils { public class WebUtils {
public static void clickByActions(SelenideElement element) { public static int getTimeout(int... timeoutInSeconds) {
log.debug("\nclickByActions: {}", element.getSearchCriteria()); return (timeoutInSeconds != null && timeoutInSeconds.length > 0) ? timeoutInSeconds[0] : 4;
element.shouldBe(Condition.enabled); }
new Actions(WebDriverRunner.getWebDriver())
.moveToElement(element)
.click(element)
.perform();
}
public static void sendKeysByActions(SelenideElement element, String keys) { public static void sendKeysAfterClear(SelenideElement element, String keys) {
log.debug("\nsendKeysByActions: {} \nsend keys '{}'", element.getSearchCriteria(), keys); log.debug("\nsendKeysAfterClear: {} \nsend keys '{}'", element.getSearchCriteria(), keys);
element.shouldBe(Condition.enabled); element.shouldBe(Condition.enabled).clear();
new Actions(WebDriverRunner.getWebDriver()) if (keys != null) {
.moveToElement(element) element.sendKeys(keys);
.sendKeys(element, keys)
.perform();
} }
}
public static void clickByJavaScript(SelenideElement element) { public static void clickByActions(SelenideElement element) {
log.debug("\nclickByJavaScript: {}", element.getSearchCriteria()); log.debug("\nclickByActions: {}", element.getSearchCriteria());
element.shouldBe(Condition.enabled); element.shouldBe(Condition.enabled);
String script = "arguments[0].click();"; new Actions(WebDriverRunner.getWebDriver())
executeJavaScript(script, element); .moveToElement(element)
} .click(element)
.perform();
}
public static void clearByKeyboard(SelenideElement field) { public static void sendKeysByActions(SelenideElement element, String keys) {
log.debug("\nclearByKeyboard: {}", field.getSearchCriteria()); log.debug("\nsendKeysByActions: {} \nsend keys '{}'", element.getSearchCriteria(), keys);
field.shouldBe(Condition.enabled).sendKeys(Keys.END); element.shouldBe(Condition.enabled);
field.sendKeys(Keys.chord(Keys.CONTROL + "a"), Keys.DELETE); new Actions(WebDriverRunner.getWebDriver())
} .moveToElement(element)
.sendKeys(element, keys)
.perform();
}
public static boolean isVisible(SelenideElement element) { public static void clickByJavaScript(SelenideElement element) {
log.debug("\nisVisible: {}", element.getSearchCriteria()); log.debug("\nclickByJavaScript: {}", element.getSearchCriteria());
boolean isVisible = false; element.shouldBe(Condition.enabled);
try { String script = "arguments[0].click();";
element.shouldBe(Condition.visible); executeJavaScript(script, element);
isVisible = true; }
} catch (Throwable e) {
log.debug("{} is not visible", element.getSearchCriteria());
}
return isVisible;
}
public static boolean isEnabled(SelenideElement element) { public static void clearByKeyboard(SelenideElement field) {
log.debug("\nisEnabled: {}", element.getSearchCriteria()); log.debug("\nclearByKeyboard: {}", field.getSearchCriteria());
boolean isEnabled = false; field.shouldBe(Condition.enabled).sendKeys(Keys.END);
try { field.sendKeys(Keys.chord(Keys.CONTROL + "a"), Keys.DELETE);
element.shouldBe(Condition.enabled); }
isEnabled = true;
} catch (Throwable e) {
log.debug("{} is not enabled", element.getSearchCriteria());
}
return isEnabled;
}
public static boolean isSelected(SelenideElement element) { public static boolean isVisible(SelenideElement element, int... timeoutInSeconds) {
log.debug("\nisSelected: {}", element.getSearchCriteria()); log.debug("\nisVisible: {}", element.getSearchCriteria());
boolean isSelected = false; boolean isVisible = false;
try { try {
element.shouldBe(Condition.selected); element.shouldBe(Condition.visible,
isSelected = true; Duration.ofSeconds(getTimeout(timeoutInSeconds)));
} catch (Throwable e) { isVisible = true;
log.debug("{} is not selected", element.getSearchCriteria()); } catch (Throwable e) {
} log.debug("{} is not visible", element.getSearchCriteria());
return isSelected;
} }
return isVisible;
}
public static boolean selectElement(SelenideElement element, boolean select) { public static boolean isEnabled(SelenideElement element, int... timeoutInSeconds) {
if (select) { log.debug("\nisEnabled: {}", element.getSearchCriteria());
if (!element.isSelected()) clickByJavaScript(element); boolean isEnabled = false;
} else { try {
if (element.isSelected()) clickByJavaScript(element); element.shouldBe(Condition.enabled,
} Duration.ofSeconds(getTimeout(timeoutInSeconds)));
return true; isEnabled = true;
} catch (Throwable e) {
log.debug("{} is not enabled", element.getSearchCriteria());
} }
return isEnabled;
}
public static boolean isSelected(SelenideElement element, int... timeoutInSeconds) {
log.debug("\nisSelected: {}", element.getSearchCriteria());
boolean isSelected = false;
try {
element.shouldBe(Condition.selected,
Duration.ofSeconds(getTimeout(timeoutInSeconds)));
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;
}
} }

View file

@ -0,0 +1,33 @@
package com.provectus.kafka.ui.utilities.qase;
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;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import lombok.extern.slf4j.Slf4j;
@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");
}
}
}

View file

@ -1,7 +1,6 @@
package com.provectus.kafka.ui.utilities.qaseUtils.annotations; package com.provectus.kafka.ui.utilities.qase.annotations;
import com.provectus.kafka.ui.utilities.qaseUtils.enums.State;
import com.provectus.kafka.ui.utilities.qase.enums.State;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@ -11,5 +10,5 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface Automation { public @interface Automation {
State state(); State state();
} }

View file

@ -1,4 +1,4 @@
package com.provectus.kafka.ui.utilities.qaseUtils.annotations; package com.provectus.kafka.ui.utilities.qase.annotations;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -9,5 +9,5 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface Status { public @interface Status {
com.provectus.kafka.ui.utilities.qaseUtils.enums.Status status(); com.provectus.kafka.ui.utilities.qase.enums.Status status();
} }

Some files were not shown because too many files have changed in this diff Show more