Merge branch 'master' of github.com:provectus/kafka-ui into feature/schema_registry_views
This commit is contained in:
commit
2cf2f6e186
23 changed files with 1417 additions and 905 deletions
|
@ -38,7 +38,7 @@ public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHan
|
|||
private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
|
||||
Map<String, Object> errorAttributes = getErrorAttributes(request, false);
|
||||
HttpStatus statusCode = Optional.ofNullable(errorAttributes.get(GlobalErrorAttributes.STATUS))
|
||||
.map(code -> (HttpStatus) code)
|
||||
.map(code -> code instanceof Integer ? HttpStatus.valueOf((Integer) code) : (HttpStatus) code)
|
||||
.orElse(HttpStatus.BAD_REQUEST);
|
||||
return ServerResponse
|
||||
.status(statusCode)
|
||||
|
|
|
@ -12,6 +12,8 @@ import com.provectus.kafka.ui.model.SchemaSubject;
|
|||
import java.util.Formatter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
@ -21,6 +23,8 @@ import org.springframework.web.reactive.function.client.WebClient;
|
|||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
|
@ -37,17 +41,30 @@ public class SchemaRegistryService {
|
|||
private final ClusterMapper mapper;
|
||||
private final WebClient webClient;
|
||||
|
||||
public Flux<String> getAllSchemaSubjects(String clusterName) {
|
||||
public Flux<SchemaSubject> getAllLatestVersionSchemas(String clusterName) {
|
||||
var allSubjectNames = getAllSubjectNames(clusterName);
|
||||
return allSubjectNames
|
||||
.flatMapMany(Flux::fromArray)
|
||||
.flatMap(subject -> getLatestSchemaSubject(clusterName, subject));
|
||||
}
|
||||
|
||||
public Mono<String[]> getAllSubjectNames(String clusterName) {
|
||||
return clustersStorage.getClusterByName(clusterName)
|
||||
.map(cluster -> webClient.get()
|
||||
.uri(cluster.getSchemaRegistry() + URL_SUBJECTS)
|
||||
.retrieve()
|
||||
.bodyToFlux(String.class)
|
||||
.doOnError(log::error))
|
||||
.orElse(Flux.error(new NotFoundException("No such cluster")));
|
||||
.bodyToMono(String[].class)
|
||||
.doOnError(log::error)
|
||||
)
|
||||
.orElse(Mono.error(new NotFoundException("No such cluster")));
|
||||
}
|
||||
|
||||
public Flux<Integer> getSchemaSubjectVersions(String clusterName, String schemaName) {
|
||||
public Flux<SchemaSubject> getAllVersionsBySubject(String clusterName, String subject) {
|
||||
Flux<Integer> versions = getSubjectVersions(clusterName, subject);
|
||||
return versions.flatMap(version -> getSchemaSubjectByVersion(clusterName, subject, version));
|
||||
}
|
||||
|
||||
private Flux<Integer> getSubjectVersions(String clusterName, String schemaName) {
|
||||
return clustersStorage.getClusterByName(clusterName)
|
||||
.map(cluster -> webClient.get()
|
||||
.uri(cluster.getSchemaRegistry() + URL_SUBJECT_VERSIONS, schemaName)
|
||||
|
|
|
@ -105,29 +105,30 @@ public class MetricsRestController implements ApiClustersApi {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResponseEntity<SchemaSubject>> getLatestSchema(String clusterName, String schemaName, ServerWebExchange exchange) {
|
||||
return schemaRegistryService.getLatestSchemaSubject(clusterName, schemaName).map(ResponseEntity::ok);
|
||||
public Mono<ResponseEntity<SchemaSubject>> getLatestSchema(String clusterName, String subject, ServerWebExchange exchange) {
|
||||
return schemaRegistryService.getLatestSchemaSubject(clusterName, subject).map(ResponseEntity::ok);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResponseEntity<SchemaSubject>> getSchemaByVersion(String clusterName, String schemaName, Integer version, ServerWebExchange exchange) {
|
||||
return schemaRegistryService.getSchemaSubjectByVersion(clusterName, schemaName, version).map(ResponseEntity::ok);
|
||||
public Mono<ResponseEntity<SchemaSubject>> getSchemaByVersion(String clusterName, String subject, Integer version, ServerWebExchange exchange) {
|
||||
return schemaRegistryService.getSchemaSubjectByVersion(clusterName, subject, version).map(ResponseEntity::ok);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResponseEntity<Flux<String>>> getSchemas(String clusterName, ServerWebExchange exchange) {
|
||||
Flux<String> subjects = schemaRegistryService.getAllSchemaSubjects(clusterName);
|
||||
public Mono<ResponseEntity<Flux<SchemaSubject>>> getSchemas(String clusterName, ServerWebExchange exchange) {
|
||||
Flux<SchemaSubject> subjects = schemaRegistryService.getAllLatestVersionSchemas(clusterName);
|
||||
return Mono.just(ResponseEntity.ok(subjects));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResponseEntity<Flux<Integer>>> getSchemaVersions(String clusterName, String subjectName, ServerWebExchange exchange) {
|
||||
return Mono.just(ResponseEntity.ok(schemaRegistryService.getSchemaSubjectVersions(clusterName, subjectName)));
|
||||
public Mono<ResponseEntity<Flux<SchemaSubject>>> getAllVersionsBySubject(String clusterName, String subjectName, ServerWebExchange exchange) {
|
||||
Flux<SchemaSubject> schemas = schemaRegistryService.getAllVersionsBySubject(clusterName, subjectName);
|
||||
return Mono.just(ResponseEntity.ok(schemas));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResponseEntity<Void>> deleteLatestSchema(String clusterName, String schemaName, ServerWebExchange exchange) {
|
||||
return schemaRegistryService.deleteLatestSchemaSubject(clusterName, schemaName);
|
||||
public Mono<ResponseEntity<Void>> deleteLatestSchema(String clusterName, String subject, ServerWebExchange exchange) {
|
||||
return schemaRegistryService.deleteLatestSchemaSubject(clusterName, subject);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -141,10 +142,10 @@ public class MetricsRestController implements ApiClustersApi {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResponseEntity<SchemaSubject>> createNewSchema(String clusterName, String schemaName,
|
||||
public Mono<ResponseEntity<SchemaSubject>> createNewSchema(String clusterName, String subject,
|
||||
@Valid Mono<NewSchemaSubject> newSchemaSubject,
|
||||
ServerWebExchange exchange) {
|
||||
return schemaRegistryService.createNewSubject(clusterName, schemaName, newSchemaSubject);
|
||||
return schemaRegistryService.createNewSubject(clusterName, subject, newSchemaSubject);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -172,17 +173,17 @@ public class MetricsRestController implements ApiClustersApi {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResponseEntity<CompatibilityCheckResponse>> checkSchemaCompatibility(String clusterName, String schemaName,
|
||||
public Mono<ResponseEntity<CompatibilityCheckResponse>> checkSchemaCompatibility(String clusterName, String subject,
|
||||
@Valid Mono<NewSchemaSubject> newSchemaSubject,
|
||||
ServerWebExchange exchange) {
|
||||
return schemaRegistryService.checksSchemaCompatibility(clusterName, schemaName, newSchemaSubject)
|
||||
return schemaRegistryService.checksSchemaCompatibility(clusterName, subject, newSchemaSubject)
|
||||
.map(ResponseEntity::ok);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResponseEntity<Void>> updateSchemaCompatibilityLevel(String clusterName, String schemaName, @Valid Mono<CompatibilityLevel> compatibilityLevel, ServerWebExchange exchange) {
|
||||
log.info("Updating schema compatibility for schema: {}", schemaName);
|
||||
return schemaRegistryService.updateSchemaCompatibility(clusterName, schemaName, compatibilityLevel)
|
||||
public Mono<ResponseEntity<Void>> updateSchemaCompatibilityLevel(String clusterName, String subject, @Valid Mono<CompatibilityLevel> compatibilityLevel, ServerWebExchange exchange) {
|
||||
log.info("Updating schema compatibility for subject: {}", subject);
|
||||
return schemaRegistryService.updateSchemaCompatibility(clusterName, subject, compatibilityLevel)
|
||||
.map(ResponseEntity::ok);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.provectus.kafka.ui.model.SchemaSubject;
|
|||
import lombok.extern.log4j.Log4j2;
|
||||
import lombok.val;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||
|
@ -23,6 +24,12 @@ import java.util.UUID;
|
|||
class SchemaRegistryServiceTests extends AbstractBaseTest {
|
||||
@Autowired
|
||||
WebTestClient webTestClient;
|
||||
String subject;
|
||||
|
||||
@BeforeEach
|
||||
void setUpBefore() {
|
||||
this.subject = UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should404WhenGetAllSchemasForUnknownCluster() {
|
||||
|
@ -34,11 +41,11 @@ class SchemaRegistryServiceTests extends AbstractBaseTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void shouldReturn404WhenGetLatestSchemaByNonExistingSchemaName() {
|
||||
void shouldReturn404WhenGetLatestSchemaByNonExistingSubject() {
|
||||
String unknownSchema = "unknown-schema";
|
||||
webTestClient
|
||||
.get()
|
||||
.uri("http://localhost:8080/api/clusters/local/schemas/{schemaName}/latest", unknownSchema)
|
||||
.uri("http://localhost:8080/api/clusters/local/schemas/{subject}/latest", unknownSchema)
|
||||
.exchange()
|
||||
.expectStatus().isNotFound();
|
||||
}
|
||||
|
@ -59,49 +66,51 @@ class SchemaRegistryServiceTests extends AbstractBaseTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnNotNullResponseWhenGetAllSchemas() {
|
||||
public void shouldReturnNotEmptyResponseWhenGetAllSchemas() {
|
||||
createNewSubjectAndAssert(subject);
|
||||
|
||||
webTestClient
|
||||
.get()
|
||||
.uri("http://localhost:8080/api/clusters/local/schemas")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBodyList(String.class)
|
||||
.expectBodyList(SchemaSubject.class)
|
||||
.consumeWith(result -> {
|
||||
List<String> responseBody = result.getResponseBody();
|
||||
Assertions.assertNotNull(responseBody);
|
||||
List<SchemaSubject> responseBody = result.getResponseBody();
|
||||
log.info("Response of test schemas: {}", responseBody);
|
||||
Assertions.assertNotNull(responseBody);
|
||||
Assertions.assertFalse(responseBody.isEmpty());
|
||||
|
||||
SchemaSubject actualSchemaSubject = responseBody.stream()
|
||||
.filter(schemaSubject -> subject.equals(schemaSubject.getSubject()))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
Assertions.assertNotNull(actualSchemaSubject.getId());
|
||||
Assertions.assertNotNull(actualSchemaSubject.getVersion());
|
||||
Assertions.assertNotNull(actualSchemaSubject.getCompatibilityLevel());
|
||||
Assertions.assertEquals("\"string\"", actualSchemaSubject.getSchema());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldOkWhenCreateNewSchemaThenGetAndUpdateItsCompatibilityLevel() {
|
||||
String schemaName = UUID.randomUUID().toString();
|
||||
// Create a new schema
|
||||
webTestClient
|
||||
.post()
|
||||
.uri("http://localhost:8080/api/clusters/local/schemas/{schemaName}", schemaName)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(BodyInserters.fromValue("{\"schema\":\"{\\\"type\\\": \\\"string\\\"}\"}"))
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(SchemaSubject.class)
|
||||
.consumeWith(this::assertResponseBodyWhenCreateNewSchema);
|
||||
createNewSubjectAndAssert(subject);
|
||||
|
||||
//Get the created schema and check its items
|
||||
webTestClient
|
||||
.get()
|
||||
.uri("http://localhost:8080/api/clusters/local/schemas/{schemaName}/latest", schemaName)
|
||||
.uri("http://localhost:8080/api/clusters/local/schemas/{subject}/latest", subject)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBodyList(SchemaSubject.class)
|
||||
.consumeWith(listEntityExchangeResult -> {
|
||||
val expectedCompatibility = CompatibilityLevel.CompatibilityEnum.BACKWARD;
|
||||
assertSchemaWhenGetLatest(schemaName, listEntityExchangeResult, expectedCompatibility);
|
||||
assertSchemaWhenGetLatest(subject, listEntityExchangeResult, expectedCompatibility);
|
||||
});
|
||||
|
||||
//Now let's change compatibility level of this schema to FULL whereas the global level should be BACKWARD
|
||||
webTestClient.put()
|
||||
.uri("http://localhost:8080/api/clusters/local/schemas/{schemaName}/compatibility", schemaName)
|
||||
.uri("http://localhost:8080/api/clusters/local/schemas/{subject}/compatibility", subject)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(BodyInserters.fromValue("{\"compatibility\":\"FULL\"}"))
|
||||
.exchange()
|
||||
|
@ -110,23 +119,35 @@ class SchemaRegistryServiceTests extends AbstractBaseTest {
|
|||
//Get one more time to check the schema compatibility level is changed to FULL
|
||||
webTestClient
|
||||
.get()
|
||||
.uri("http://localhost:8080/api/clusters/local/schemas/{schemaName}/latest", schemaName)
|
||||
.uri("http://localhost:8080/api/clusters/local/schemas/{subject}/latest", subject)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBodyList(SchemaSubject.class)
|
||||
.consumeWith(listEntityExchangeResult -> {
|
||||
val expectedCompatibility = CompatibilityLevel.CompatibilityEnum.FULL;
|
||||
assertSchemaWhenGetLatest(schemaName, listEntityExchangeResult, expectedCompatibility);
|
||||
assertSchemaWhenGetLatest(subject, listEntityExchangeResult, expectedCompatibility);
|
||||
});
|
||||
}
|
||||
|
||||
private void assertSchemaWhenGetLatest(String schemaName, EntityExchangeResult<List<SchemaSubject>> listEntityExchangeResult, CompatibilityLevel.CompatibilityEnum expectedCompatibility) {
|
||||
private void createNewSubjectAndAssert(String subject) {
|
||||
webTestClient
|
||||
.post()
|
||||
.uri("http://localhost:8080/api/clusters/local/schemas/{subject}", subject)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(BodyInserters.fromValue("{\"schema\":\"{\\\"type\\\": \\\"string\\\"}\"}"))
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(SchemaSubject.class)
|
||||
.consumeWith(this::assertResponseBodyWhenCreateNewSchema);
|
||||
}
|
||||
|
||||
private void assertSchemaWhenGetLatest(String subject, EntityExchangeResult<List<SchemaSubject>> listEntityExchangeResult, CompatibilityLevel.CompatibilityEnum expectedCompatibility) {
|
||||
List<SchemaSubject> responseBody = listEntityExchangeResult.getResponseBody();
|
||||
Assertions.assertNotNull(responseBody);
|
||||
Assertions.assertEquals(1, responseBody.size());
|
||||
SchemaSubject actualSchema = responseBody.get(0);
|
||||
Assertions.assertNotNull(actualSchema);
|
||||
Assertions.assertEquals(schemaName, actualSchema.getSubject());
|
||||
Assertions.assertEquals(subject, actualSchema.getSubject());
|
||||
Assertions.assertEquals("\"string\"", actualSchema.getSchema());
|
||||
|
||||
Assertions.assertNotNull(actualSchema.getCompatibilityLevel());
|
||||
|
|
|
@ -339,7 +339,7 @@ paths:
|
|||
get:
|
||||
tags:
|
||||
- /api/clusters
|
||||
summary: get all schemas from Schema Registry service
|
||||
summary: get all schemas of latest version from Schema Registry service
|
||||
operationId: getSchemas
|
||||
parameters:
|
||||
- name: clusterName
|
||||
|
@ -355,9 +355,9 @@ paths:
|
|||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
$ref: '#/components/schemas/SchemaSubject'
|
||||
|
||||
/api/clusters/{clusterName}/schemas/{schemaName}:
|
||||
/api/clusters/{clusterName}/schemas/{subject}:
|
||||
post:
|
||||
tags:
|
||||
- /api/clusters
|
||||
|
@ -369,7 +369,7 @@ paths:
|
|||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: schemaName
|
||||
- name: subject
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
|
@ -399,7 +399,7 @@ paths:
|
|||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: schemaName
|
||||
- name: subject
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
|
@ -410,19 +410,19 @@ paths:
|
|||
404:
|
||||
description: Not found
|
||||
|
||||
/api/clusters/{clusterName}/schemas/{schemaName}/versions:
|
||||
/api/clusters/{clusterName}/schemas/{subject}/versions:
|
||||
get:
|
||||
tags:
|
||||
- /api/clusters
|
||||
summary: get all version of schema from Schema Registry service
|
||||
operationId: getSchemaVersions
|
||||
summary: get all version of subject from Schema Registry service
|
||||
operationId: getAllVersionsBySubject
|
||||
parameters:
|
||||
- name: clusterName
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: schemaName
|
||||
- name: subject
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
|
@ -435,9 +435,9 @@ paths:
|
|||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
$ref: '#/components/schemas/SchemaSubject'
|
||||
|
||||
/api/clusters/{clusterName}/schemas/{schemaName}/latest:
|
||||
/api/clusters/{clusterName}/schemas/{subject}/latest:
|
||||
get:
|
||||
tags:
|
||||
- /api/clusters
|
||||
|
@ -449,7 +449,7 @@ paths:
|
|||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: schemaName
|
||||
- name: subject
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
|
@ -472,7 +472,7 @@ paths:
|
|||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: schemaName
|
||||
- name: subject
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
|
@ -484,7 +484,7 @@ paths:
|
|||
description: Not found
|
||||
|
||||
|
||||
/api/clusters/{clusterName}/schemas/{schemaName}/versions/{version}:
|
||||
/api/clusters/{clusterName}/schemas/{subject}/versions/{version}:
|
||||
get:
|
||||
tags:
|
||||
- /api/clusters
|
||||
|
@ -496,7 +496,7 @@ paths:
|
|||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: schemaName
|
||||
- name: subject
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
|
@ -524,7 +524,7 @@ paths:
|
|||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: schemaName
|
||||
- name: subject
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
|
@ -581,7 +581,7 @@ paths:
|
|||
404:
|
||||
description: Not Found
|
||||
|
||||
/api/clusters/{clusterName}/schemas/{schemaName}/compatibility:
|
||||
/api/clusters/{clusterName}/schemas/{subject}/compatibility:
|
||||
put:
|
||||
tags:
|
||||
- /api/clusters
|
||||
|
@ -593,7 +593,7 @@ paths:
|
|||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: schemaName
|
||||
- name: subject
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
|
@ -609,7 +609,7 @@ paths:
|
|||
404:
|
||||
description: Not Found
|
||||
|
||||
/api/clusters/{clusterName}/schemas/{schemaName}/check:
|
||||
/api/clusters/{clusterName}/schemas/{subject}/check:
|
||||
post:
|
||||
tags:
|
||||
- /api/clusters
|
||||
|
@ -621,7 +621,7 @@ paths:
|
|||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: schemaName
|
||||
- name: subject
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
|
|
|
@ -15,7 +15,10 @@
|
|||
},
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module",
|
||||
"project": ["./tsconfig.json", "./src/setupTests.ts"]
|
||||
"project": [
|
||||
"./tsconfig.json",
|
||||
"./src/setupTests.ts"
|
||||
]
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"extends": [
|
||||
|
@ -30,7 +33,8 @@
|
|||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"jsx-a11y/label-has-associated-control": "off",
|
||||
"import/prefer-default-export": "off",
|
||||
"@typescript-eslint/no-explicit-any": "error"
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
module.exports = {
|
||||
roots: ['<rootDir>/src'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'ts-jest',
|
||||
},
|
||||
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
};
|
1434
kafka-ui-react-app/package-lock.json
generated
1434
kafka-ui-react-app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -3,8 +3,6 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@types/react-datepicker": "^3.1.1",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"bulma": "^0.8.2",
|
||||
"bulma-switch": "^2.0.0",
|
||||
"classnames": "^2.2.6",
|
||||
|
@ -33,7 +31,7 @@
|
|||
"*.{js,ts,jsx,tsx}": [
|
||||
"eslint -c .eslintrc.json --fix",
|
||||
"git add",
|
||||
"jest --bail --findRelatedTests"
|
||||
"npm test -- --bail --findRelatedTests --watchAll=false"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
|
@ -42,6 +40,7 @@
|
|||
"lint": "eslint --ext .tsx,.ts src/",
|
||||
"lint:fix": "eslint --ext .tsx,.ts src/ --fix",
|
||||
"test": "react-scripts test",
|
||||
"test:CI": "CI=true npm test --watchAll=false",
|
||||
"eject": "react-scripts eject",
|
||||
"tsc": "tsc"
|
||||
},
|
||||
|
@ -66,6 +65,7 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/types": "^26.6.2",
|
||||
"@testing-library/jest-dom": "^5.11.9",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
|
@ -75,16 +75,19 @@
|
|||
"@types/lodash": "^4.14.165",
|
||||
"@types/node": "^12.19.8",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-datepicker": "^3.1.1",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-redux": "^7.1.11",
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
"@types/redux": "^3.6.0",
|
||||
"@types/redux-thunk": "^2.1.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.9.0",
|
||||
"@typescript-eslint/parser": "^4.9.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.4.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-to-json": "^3.6.1",
|
||||
"eslint": "^7.14.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-airbnb-typescript": "^12.0.0",
|
||||
|
@ -99,12 +102,16 @@
|
|||
"lint-staged": "^10.5.2",
|
||||
"node-sass": "^4.14.1",
|
||||
"prettier": "^2.2.1",
|
||||
"react-scripts": "4.0.1",
|
||||
"react-scripts": "4.0.2",
|
||||
"ts-jest": "^26.4.4",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "~4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.15.4"
|
||||
},
|
||||
"proxy": "http://localhost:8080"
|
||||
"proxy": "http://localhost:8080",
|
||||
"jest": {
|
||||
"snapshotSerializers": ["enzyme-to-json/serializer"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@ import { format } from 'date-fns';
|
|||
import JSONTree from 'react-json-tree';
|
||||
import { TopicMessage } from 'generated-sources';
|
||||
|
||||
interface MessageItemProp {
|
||||
export interface MessageItemProp {
|
||||
partition: TopicMessage['partition'];
|
||||
offset: TopicMessage['offset'];
|
||||
timestamp: TopicMessage['timestamp'];
|
||||
content: TopicMessage['content'];
|
||||
content?: TopicMessage['content'];
|
||||
}
|
||||
|
||||
const MessageItem: React.FC<MessageItemProp> = ({
|
||||
|
@ -16,13 +16,11 @@ const MessageItem: React.FC<MessageItemProp> = ({
|
|||
timestamp,
|
||||
content,
|
||||
}) => (
|
||||
<tr key="{timestamp}">
|
||||
<td style={{ width: 200 }}>
|
||||
{timestamp ? format(timestamp, 'yyyy-MM-dd HH:mm:ss') : null}
|
||||
</td>
|
||||
<tr>
|
||||
<td style={{ width: 200 }}>{format(timestamp, 'yyyy-MM-dd HH:mm:ss')}</td>
|
||||
<td style={{ width: 150 }}>{offset}</td>
|
||||
<td style={{ width: 100 }}>{partition}</td>
|
||||
<td key="{content}" style={{ wordBreak: 'break-word' }}>
|
||||
<td style={{ wordBreak: 'break-word' }}>
|
||||
{content && (
|
||||
<JSONTree
|
||||
data={content}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { groupBy, map, concat, maxBy } from 'lodash';
|
||||
import MultiSelect from 'react-multi-select-component';
|
||||
import { Option } from 'react-multi-select-component/dist/lib/interfaces';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import {
|
||||
ClusterName,
|
||||
TopicMessageQueryParams,
|
||||
|
@ -7,13 +12,6 @@ import {
|
|||
import { TopicMessage, Partition, SeekType } from 'generated-sources';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
import MultiSelect from 'react-multi-select-component';
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { Option } from 'react-multi-select-component/dist/lib/interfaces';
|
||||
import MessagesTable from './MessagesTable';
|
||||
|
||||
export interface Props {
|
||||
|
@ -81,17 +79,17 @@ const Messages: React.FC<Props> = ({
|
|||
offset: 0,
|
||||
partition: p.partition,
|
||||
}));
|
||||
const messageUniqs: FilterProps[] = _.map(
|
||||
_.groupBy(messages, 'partition'),
|
||||
(v) => _.maxBy(v, 'offset')
|
||||
const messageUniqs: FilterProps[] = map(
|
||||
groupBy(messages, 'partition'),
|
||||
(v) => maxBy(v, 'offset')
|
||||
).map((v) => ({
|
||||
offset: v ? v.offset : 0,
|
||||
partition: v ? v.partition : 0,
|
||||
}));
|
||||
|
||||
return _.map(
|
||||
_.groupBy(_.concat(partitionUniqs, messageUniqs), 'partition'),
|
||||
(v) => _.maxBy(v, 'offset') as FilterProps
|
||||
return map(
|
||||
groupBy(concat(partitionUniqs, messageUniqs), 'partition'),
|
||||
(v) => maxBy(v, 'offset') as FilterProps
|
||||
);
|
||||
}, [messages, partitions]);
|
||||
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
ClusterName,
|
||||
RootState,
|
||||
TopicMessageQueryParams,
|
||||
TopicName,
|
||||
} from 'redux/interfaces';
|
||||
import { ClusterName, RootState, TopicName } from 'redux/interfaces';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { fetchTopicMessages } from 'redux/actions';
|
||||
import {
|
||||
|
@ -38,11 +33,7 @@ const mapStateToProps = (
|
|||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchTopicMessages: (
|
||||
clusterName: ClusterName,
|
||||
topicName: TopicName,
|
||||
queryParams: Partial<TopicMessageQueryParams>
|
||||
) => fetchTopicMessages(clusterName, topicName, queryParams),
|
||||
fetchTopicMessages,
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
|
|
|
@ -3,7 +3,7 @@ import { TopicMessage } from 'generated-sources';
|
|||
import CustomParamButton from 'components/Topics/shared/Form/CustomParams/CustomParamButton';
|
||||
import MessageItem from './MessageItem';
|
||||
|
||||
interface MessagesTableProp {
|
||||
export interface MessagesTableProp {
|
||||
messages: TopicMessage[];
|
||||
onNext(event: React.MouseEvent<HTMLButtonElement>): void;
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ const MessagesTable: React.FC<MessagesTableProp> = ({ messages, onNext }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<table className="table is-striped is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -28,7 +28,7 @@ const MessagesTable: React.FC<MessagesTableProp> = ({ messages, onNext }) => {
|
|||
{messages.map(
|
||||
({ partition, offset, timestamp, content }: TopicMessage) => (
|
||||
<MessageItem
|
||||
key={timestamp.toString()}
|
||||
key={`message-${timestamp.getTime()}`}
|
||||
partition={partition}
|
||||
offset={offset}
|
||||
timestamp={timestamp}
|
||||
|
@ -48,7 +48,7 @@ const MessagesTable: React.FC<MessagesTableProp> = ({ messages, onNext }) => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import MessageItem from 'components/Topics/Details/Messages/MessageItem';
|
||||
import { messages } from './fixtures';
|
||||
|
||||
jest.mock('date-fns', () => ({
|
||||
format: () => `mocked date`,
|
||||
}));
|
||||
|
||||
describe('MessageItem', () => {
|
||||
describe('when content is defined', () => {
|
||||
it('renders table row with JSONTree', () => {
|
||||
const wrapper = shallow(<MessageItem {...messages[0]} />);
|
||||
|
||||
expect(wrapper.find('tr').length).toEqual(1);
|
||||
expect(wrapper.find('td').length).toEqual(4);
|
||||
expect(wrapper.find('JSONTree').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(shallow(<MessageItem {...messages[0]} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when content is undefined', () => {
|
||||
it('renders table row without JSONTree', () => {
|
||||
const wrapper = shallow(<MessageItem {...messages[1]} />);
|
||||
|
||||
expect(wrapper.find('tr').length).toEqual(1);
|
||||
expect(wrapper.find('td').length).toEqual(4);
|
||||
expect(wrapper.find('JSONTree').length).toEqual(0);
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(shallow(<MessageItem {...messages[1]} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,178 @@
|
|||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import * as useDebounce from 'use-debounce';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import Messages, { Props } from 'components/Topics/Details/Messages/Messages';
|
||||
import MessagesContainer from 'components/Topics/Details/Messages/MessagesContainer';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import configureStore from 'redux/store/configureStore';
|
||||
|
||||
describe('Messages', () => {
|
||||
describe('Container', () => {
|
||||
const store = configureStore();
|
||||
|
||||
it('renders view', () => {
|
||||
const component = shallow(
|
||||
<Provider store={store}>
|
||||
<MessagesContainer />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
expect(component.exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('View', () => {
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const setupWrapper = (props: Partial<Props> = {}) => (
|
||||
<Messages
|
||||
clusterName="Test cluster"
|
||||
topicName="Cluster topic"
|
||||
isFetched
|
||||
fetchTopicMessages={jest.fn()}
|
||||
messages={[]}
|
||||
partitions={[]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
describe('Initial state', () => {
|
||||
it('renders PageLoader', () => {
|
||||
expect(
|
||||
shallow(setupWrapper({ isFetched: false })).exists(PageLoader)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Table', () => {
|
||||
describe('With messages', () => {
|
||||
const messagesWrapper = mount(
|
||||
setupWrapper({
|
||||
messages: [
|
||||
{
|
||||
partition: 1,
|
||||
offset: 2,
|
||||
timestamp: new Date('05-05-1994'),
|
||||
content: [1, 2, 3],
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
it('renders table', () => {
|
||||
expect(
|
||||
messagesWrapper.exists(
|
||||
'[className="table is-striped is-fullwidth"]'
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
it('renders JSONTree', () => {
|
||||
expect(messagesWrapper.find('JSONTree').length).toEqual(1);
|
||||
});
|
||||
it('parses message content correctly', () => {
|
||||
const messages = [
|
||||
{
|
||||
partition: 1,
|
||||
offset: 2,
|
||||
timestamp: new Date('05-05-1994'),
|
||||
content: [1, 2, 3],
|
||||
},
|
||||
];
|
||||
const content = JSON.stringify(messages[0].content);
|
||||
expect(JSON.parse(content)).toEqual(messages[0].content);
|
||||
});
|
||||
});
|
||||
describe('Without messages', () => {
|
||||
it('renders string', () => {
|
||||
const wrapper = mount(setupWrapper());
|
||||
expect(wrapper.text()).toContain('No messages at selected topic');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Offset field', () => {
|
||||
describe('Seek Type dependency', () => {
|
||||
const wrapper = mount(setupWrapper());
|
||||
|
||||
it('renders DatePicker', () => {
|
||||
wrapper
|
||||
.find('[id="selectSeekType"]')
|
||||
.simulate('change', { target: { value: 'TIMESTAMP' } });
|
||||
|
||||
expect(
|
||||
wrapper.find('[id="selectSeekType"]').first().props().value
|
||||
).toEqual('TIMESTAMP');
|
||||
|
||||
expect(wrapper.exists(DatePicker)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('With defined offset value', () => {
|
||||
const wrapper = shallow(setupWrapper());
|
||||
|
||||
it('shows offset value in input', () => {
|
||||
const offset = '10';
|
||||
|
||||
wrapper
|
||||
.find('#searchOffset')
|
||||
.simulate('change', { target: { value: offset } });
|
||||
|
||||
expect(wrapper.find('#searchOffset').first().props().value).toEqual(
|
||||
offset
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('With invalid offset value', () => {
|
||||
const wrapper = shallow(setupWrapper());
|
||||
|
||||
it('shows 0 in input', () => {
|
||||
wrapper
|
||||
.find('#searchOffset')
|
||||
.simulate('change', { target: { value: null } });
|
||||
|
||||
expect(wrapper.find('#searchOffset').first().props().value).toBe('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search field', () => {
|
||||
it('renders input correctly', () => {
|
||||
const query = 20;
|
||||
const mockedUseDebouncedCallback = jest.fn();
|
||||
jest
|
||||
.spyOn(useDebounce, 'useDebouncedCallback')
|
||||
.mockImplementationOnce(() => [
|
||||
mockedUseDebouncedCallback,
|
||||
jest.fn(),
|
||||
jest.fn(),
|
||||
]);
|
||||
|
||||
const wrapper = shallow(setupWrapper());
|
||||
|
||||
wrapper
|
||||
.find('#searchText')
|
||||
.simulate('change', { target: { value: query } });
|
||||
|
||||
expect(wrapper.find('#searchText').first().props().value).toEqual(
|
||||
query
|
||||
);
|
||||
expect(mockedUseDebouncedCallback).toHaveBeenCalledWith({ q: query });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Submit button', () => {
|
||||
it('fetches topic messages', () => {
|
||||
const mockedfetchTopicMessages = jest.fn();
|
||||
const wrapper = mount(
|
||||
setupWrapper({ fetchTopicMessages: mockedfetchTopicMessages })
|
||||
);
|
||||
|
||||
wrapper.find('[type="submit"]').simulate('click');
|
||||
expect(mockedfetchTopicMessages).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import MessagesTable, {
|
||||
MessagesTableProp,
|
||||
} from 'components/Topics/Details/Messages/MessagesTable';
|
||||
import { messages } from './fixtures';
|
||||
|
||||
jest.mock('date-fns', () => ({
|
||||
format: () => `mocked date`,
|
||||
}));
|
||||
|
||||
describe('MessagesTable', () => {
|
||||
const setupWrapper = (props: Partial<MessagesTableProp> = {}) => (
|
||||
<MessagesTable messages={[]} onNext={jest.fn()} {...props} />
|
||||
);
|
||||
|
||||
describe('when topic is empty', () => {
|
||||
it('renders table row with JSONTree', () => {
|
||||
const wrapper = shallow(setupWrapper());
|
||||
expect(wrapper.exists('table')).toBeFalsy();
|
||||
expect(wrapper.exists('CustomParamButton')).toBeFalsy();
|
||||
expect(wrapper.text()).toEqual('No messages at selected topic');
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(shallow(setupWrapper())).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when topic contains messages', () => {
|
||||
const onNext = jest.fn();
|
||||
const wrapper = shallow(setupWrapper({ messages, onNext }));
|
||||
|
||||
it('renders table row without JSONTree', () => {
|
||||
expect(wrapper.exists('table')).toBeTruthy();
|
||||
expect(wrapper.exists('CustomParamButton')).toBeTruthy();
|
||||
expect(wrapper.find('MessageItem').length).toEqual(2);
|
||||
});
|
||||
|
||||
it('handles CustomParamButton click', () => {
|
||||
wrapper.find('CustomParamButton').simulate('click');
|
||||
expect(onNext).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(shallow(setupWrapper({ messages, onNext }))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,110 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MessageItem when content is defined matches snapshot 1`] = `
|
||||
<tr>
|
||||
<td
|
||||
style={
|
||||
Object {
|
||||
"width": 200,
|
||||
}
|
||||
}
|
||||
>
|
||||
mocked date
|
||||
</td>
|
||||
<td
|
||||
style={
|
||||
Object {
|
||||
"width": 150,
|
||||
}
|
||||
}
|
||||
>
|
||||
2
|
||||
</td>
|
||||
<td
|
||||
style={
|
||||
Object {
|
||||
"width": 100,
|
||||
}
|
||||
}
|
||||
>
|
||||
1
|
||||
</td>
|
||||
<td
|
||||
style={
|
||||
Object {
|
||||
"wordBreak": "break-word",
|
||||
}
|
||||
}
|
||||
>
|
||||
<JSONTree
|
||||
collectionLimit={50}
|
||||
data={
|
||||
Object {
|
||||
"foo": "bar",
|
||||
"key": "val",
|
||||
}
|
||||
}
|
||||
getItemString={[Function]}
|
||||
hideRoot={true}
|
||||
invertTheme={false}
|
||||
isCustomNode={[Function]}
|
||||
keyPath={
|
||||
Array [
|
||||
"root",
|
||||
]
|
||||
}
|
||||
labelRenderer={[Function]}
|
||||
postprocessValue={[Function]}
|
||||
shouldExpandNode={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"base0B": "#363636",
|
||||
"base0D": "#3273dc",
|
||||
"tree": [Function],
|
||||
"value": [Function],
|
||||
}
|
||||
}
|
||||
valueRenderer={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
exports[`MessageItem when content is undefined matches snapshot 1`] = `
|
||||
<tr>
|
||||
<td
|
||||
style={
|
||||
Object {
|
||||
"width": 200,
|
||||
}
|
||||
}
|
||||
>
|
||||
mocked date
|
||||
</td>
|
||||
<td
|
||||
style={
|
||||
Object {
|
||||
"width": 150,
|
||||
}
|
||||
}
|
||||
>
|
||||
20
|
||||
</td>
|
||||
<td
|
||||
style={
|
||||
Object {
|
||||
"width": 100,
|
||||
}
|
||||
}
|
||||
>
|
||||
2
|
||||
</td>
|
||||
<td
|
||||
style={
|
||||
Object {
|
||||
"wordBreak": "break-word",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</tr>
|
||||
`;
|
|
@ -0,0 +1,66 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MessagesTable when topic contains messages matches snapshot 1`] = `
|
||||
<Fragment>
|
||||
<table
|
||||
className="table is-striped is-fullwidth"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Timestamp
|
||||
</th>
|
||||
<th>
|
||||
Offset
|
||||
</th>
|
||||
<th>
|
||||
Partition
|
||||
</th>
|
||||
<th>
|
||||
Content
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<MessageItem
|
||||
content={
|
||||
Object {
|
||||
"foo": "bar",
|
||||
"key": "val",
|
||||
}
|
||||
}
|
||||
key="message-802310400000"
|
||||
offset={2}
|
||||
partition={1}
|
||||
timestamp={1995-06-05T00:00:00.000Z}
|
||||
/>
|
||||
<MessageItem
|
||||
key="message-1596585600000"
|
||||
offset={20}
|
||||
partition={2}
|
||||
timestamp={2020-08-05T00:00:00.000Z}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
className="columns"
|
||||
>
|
||||
<div
|
||||
className="column is-full"
|
||||
>
|
||||
<CustomParamButton
|
||||
btnText="Next"
|
||||
className="is-link is-pulled-right"
|
||||
onClick={[MockFunction]}
|
||||
type="fa-chevron-right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`MessagesTable when topic is empty matches snapshot 1`] = `
|
||||
<div>
|
||||
No messages at selected topic
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,19 @@
|
|||
import { TopicMessage } from 'generated-sources';
|
||||
|
||||
export const messages: TopicMessage[] = [
|
||||
{
|
||||
partition: 1,
|
||||
offset: 2,
|
||||
timestamp: new Date(Date.UTC(1995, 5, 5)),
|
||||
content: {
|
||||
foo: 'bar',
|
||||
key: 'val',
|
||||
},
|
||||
},
|
||||
{
|
||||
partition: 2,
|
||||
offset: 20,
|
||||
timestamp: new Date(Date.UTC(2020, 7, 5)),
|
||||
content: undefined,
|
||||
},
|
||||
];
|
|
@ -88,7 +88,7 @@ const CustomParams: React.FC<Props> = ({ isSubmitting, config }) => {
|
|||
|
||||
{formCustomParams.allIndexes.map((index) => (
|
||||
<CustomParamField
|
||||
key={index}
|
||||
key={formCustomParams.byIndex[index].name}
|
||||
index={index}
|
||||
isDisabled={isSubmitting}
|
||||
name={formCustomParams.byIndex[index].name}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/* eslint-disable */
|
||||
import * as Enzyme from 'enzyme';
|
||||
import { configure } from 'enzyme';
|
||||
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
configure({ adapter: new Adapter() });
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
import React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import JSONTree from 'react-json-tree';
|
||||
import * as useDebounce from 'use-debounce';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import Messages, { Props } from 'components/Topics/Details/Messages/Messages';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
|
||||
describe('Messages component', () => {
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const setupWrapper = (props: Partial<Props> = {}) => (
|
||||
<Messages
|
||||
clusterName="Test cluster"
|
||||
topicName="Cluster topic"
|
||||
isFetched
|
||||
fetchTopicMessages={jest.fn()}
|
||||
messages={[]}
|
||||
partitions={[]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
describe('Initial state', () => {
|
||||
it('renders PageLoader', () => {
|
||||
expect(
|
||||
shallow(setupWrapper({ isFetched: false })).exists(PageLoader)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Messages table', () => {
|
||||
describe('With messages', () => {
|
||||
const messagesWrapper = mount(
|
||||
setupWrapper({
|
||||
messages: [
|
||||
{
|
||||
partition: 1,
|
||||
offset: 2,
|
||||
timestamp: new Date('05-05-1994'),
|
||||
content: [1, 2, 3],
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
it('renders table', () => {
|
||||
expect(
|
||||
messagesWrapper.exists('[className="table is-striped is-fullwidth"]')
|
||||
).toBeTruthy();
|
||||
});
|
||||
it('renders JSONTree', () => {
|
||||
expect(messagesWrapper.find(JSONTree).length).toEqual(1);
|
||||
});
|
||||
it('parses message content correctly', () => {
|
||||
const messages = [
|
||||
{
|
||||
partition: 1,
|
||||
offset: 2,
|
||||
timestamp: new Date('05-05-1994'),
|
||||
content: [1, 2, 3],
|
||||
},
|
||||
];
|
||||
const content = JSON.stringify(messages[0].content);
|
||||
expect(JSON.parse(content)).toEqual(messages[0].content);
|
||||
});
|
||||
});
|
||||
describe('Without messages', () => {
|
||||
it('renders string', () => {
|
||||
const wrapper = mount(setupWrapper());
|
||||
expect(wrapper.text()).toContain('No messages at selected topic');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Offset field', () => {
|
||||
describe('Seek Type dependency', () => {
|
||||
const wrapper = mount(setupWrapper());
|
||||
|
||||
it('renders DatePicker', () => {
|
||||
wrapper
|
||||
.find('[id="selectSeekType"]')
|
||||
.simulate('change', { target: { value: 'TIMESTAMP' } });
|
||||
|
||||
expect(
|
||||
wrapper.find('[id="selectSeekType"]').first().props().value
|
||||
).toEqual('TIMESTAMP');
|
||||
|
||||
expect(wrapper.exists(DatePicker)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('With defined offset value', () => {
|
||||
const wrapper = shallow(setupWrapper());
|
||||
|
||||
it('shows offset value in input', () => {
|
||||
const offset = '10';
|
||||
|
||||
wrapper
|
||||
.find('#searchOffset')
|
||||
.simulate('change', { target: { value: offset } });
|
||||
|
||||
expect(wrapper.find('#searchOffset').first().props().value).toEqual(
|
||||
offset
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('With invalid offset value', () => {
|
||||
const wrapper = shallow(setupWrapper());
|
||||
|
||||
it('shows 0 in input', () => {
|
||||
wrapper
|
||||
.find('#searchOffset')
|
||||
.simulate('change', { target: { value: null } });
|
||||
|
||||
expect(wrapper.find('#searchOffset').first().props().value).toBe('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search field', () => {
|
||||
it('renders input correctly', () => {
|
||||
const query = 20;
|
||||
const mockedUseDebouncedCallback = jest.fn();
|
||||
jest
|
||||
.spyOn(useDebounce, 'useDebouncedCallback')
|
||||
.mockImplementationOnce(() => [
|
||||
mockedUseDebouncedCallback,
|
||||
jest.fn(),
|
||||
jest.fn(),
|
||||
]);
|
||||
|
||||
const wrapper = shallow(setupWrapper());
|
||||
|
||||
wrapper
|
||||
.find('#searchText')
|
||||
.simulate('change', { target: { value: query } });
|
||||
|
||||
expect(wrapper.find('#searchText').first().props().value).toEqual(query);
|
||||
expect(mockedUseDebouncedCallback).toHaveBeenCalledWith({ q: query });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Submit button', () => {
|
||||
it('fetches topic messages', () => {
|
||||
const mockedfetchTopicMessages = jest.fn();
|
||||
const wrapper = mount(
|
||||
setupWrapper({ fetchTopicMessages: mockedfetchTopicMessages })
|
||||
);
|
||||
|
||||
wrapper.find('[type="submit"]').simulate('click');
|
||||
expect(mockedfetchTopicMessages).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -22,6 +22,6 @@
|
|||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
"src",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue