Schema registry pagination and search
* [ISSUE-1191, ISSUE-1208] Implemented search and pagination for schema registry overview page. * [ISSUE-1191, ISSUE-1208] Implemented search and pagination for schema registry overview page. * [ISSUE-1191, ISSUE-1208] Implemented search and pagination for schema registry overview page. * [ISSUE-1191, ISSUE-1208] fixed Checkstyle violation issue. * WIP: Fixes some frontend issues just to build frontend * WIP: Fixes fronted just to build it * WIP: Fixes frontend to build it * WIP: Schemas tests are failing * WIP: List tests work * WIP: Details test work * WIP: Updates tests * WIP: Fixes lint errors and comments * WIP: Changes usePagination, some tests have warns * WIP: Refreshes with query string works correctly * cleanup * WIP: cleanup * WIP: cleanup * WIP: Removes ThemeProvider from test as render function uses ThemeProvider * WIP: Pagination + Search works correcly * WIP: Cleanup * WIP: Cleanup * WIP: Cleanup Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com> Co-authored-by: Damir Abdulganiev <dabdulganiev@provectus.com> Co-authored-by: Damir Abdulganiev <damupka@gmail.com>
This commit is contained in:
parent
a24696cc30
commit
540b8eb79b
24 changed files with 766 additions and 1107 deletions
|
@ -7,10 +7,17 @@ import com.provectus.kafka.ui.model.CompatibilityLevelDTO;
|
||||||
import com.provectus.kafka.ui.model.KafkaCluster;
|
import com.provectus.kafka.ui.model.KafkaCluster;
|
||||||
import com.provectus.kafka.ui.model.NewSchemaSubjectDTO;
|
import com.provectus.kafka.ui.model.NewSchemaSubjectDTO;
|
||||||
import com.provectus.kafka.ui.model.SchemaSubjectDTO;
|
import com.provectus.kafka.ui.model.SchemaSubjectDTO;
|
||||||
|
import com.provectus.kafka.ui.model.SchemaSubjectsResponseDTO;
|
||||||
import com.provectus.kafka.ui.service.SchemaRegistryService;
|
import com.provectus.kafka.ui.service.SchemaRegistryService;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
@ -22,6 +29,8 @@ import reactor.core.publisher.Mono;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class SchemasController extends AbstractController implements SchemasApi {
|
public class SchemasController extends AbstractController implements SchemasApi {
|
||||||
|
|
||||||
|
private static final Integer DEFAULT_PAGE_SIZE = 25;
|
||||||
|
|
||||||
private final SchemaRegistryService schemaRegistryService;
|
private final SchemaRegistryService schemaRegistryService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -102,12 +111,29 @@ public class SchemasController extends AbstractController implements SchemasApi
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<ResponseEntity<Flux<SchemaSubjectDTO>>> getSchemas(String clusterName,
|
public Mono<ResponseEntity<SchemaSubjectsResponseDTO>> getSchemas(String clusterName,
|
||||||
ServerWebExchange exchange) {
|
@Valid Integer pageNum,
|
||||||
Flux<SchemaSubjectDTO> subjects = schemaRegistryService.getAllLatestVersionSchemas(
|
@Valid Integer perPage,
|
||||||
getCluster(clusterName)
|
@Valid String search,
|
||||||
);
|
ServerWebExchange serverWebExchange) {
|
||||||
return Mono.just(ResponseEntity.ok(subjects));
|
return schemaRegistryService
|
||||||
|
.getAllSubjectNames(getCluster(clusterName))
|
||||||
|
.flatMap(subjects -> {
|
||||||
|
int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE;
|
||||||
|
int subjectToSkip = ((pageNum != null && pageNum > 0 ? pageNum : 1) - 1) * pageSize;
|
||||||
|
List<String> filteredSubjects = Arrays.stream(subjects)
|
||||||
|
.filter(subj -> search == null || StringUtils.containsIgnoreCase(subj, search))
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
var totalPages = (filteredSubjects.size() / pageSize)
|
||||||
|
+ (filteredSubjects.size() % pageSize == 0 ? 0 : 1);
|
||||||
|
List<String> subjectsToRender = filteredSubjects.stream()
|
||||||
|
.skip(subjectToSkip)
|
||||||
|
.limit(pageSize)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return schemaRegistryService.getAllLatestVersionSchemas(getCluster(clusterName), subjectsToRender)
|
||||||
|
.map(a -> new SchemaSubjectsResponseDTO().pageCount(totalPages).schemas(a));
|
||||||
|
}).map(ResponseEntity::ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -22,9 +22,11 @@ import com.provectus.kafka.ui.model.schemaregistry.InternalCompatibilityLevel;
|
||||||
import com.provectus.kafka.ui.model.schemaregistry.InternalNewSchema;
|
import com.provectus.kafka.ui.model.schemaregistry.InternalNewSchema;
|
||||||
import com.provectus.kafka.ui.model.schemaregistry.SubjectIdResponse;
|
import com.provectus.kafka.ui.model.schemaregistry.SubjectIdResponse;
|
||||||
import java.util.Formatter;
|
import java.util.Formatter;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
@ -43,6 +45,7 @@ import reactor.core.publisher.Mono;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SchemaRegistryService {
|
public class SchemaRegistryService {
|
||||||
|
|
||||||
public static final String NO_SUCH_SCHEMA_VERSION = "No such schema %s with version %s";
|
public static final String NO_SUCH_SCHEMA_VERSION = "No such schema %s with version %s";
|
||||||
public static final String NO_SUCH_SCHEMA = "No such schema %s";
|
public static final String NO_SUCH_SCHEMA = "No such schema %s";
|
||||||
|
|
||||||
|
@ -57,11 +60,11 @@ public class SchemaRegistryService {
|
||||||
private final ClusterMapper mapper;
|
private final ClusterMapper mapper;
|
||||||
private final WebClient webClient;
|
private final WebClient webClient;
|
||||||
|
|
||||||
public Flux<SchemaSubjectDTO> getAllLatestVersionSchemas(KafkaCluster cluster) {
|
public Mono<List<SchemaSubjectDTO>> getAllLatestVersionSchemas(KafkaCluster cluster,
|
||||||
var allSubjectNames = getAllSubjectNames(cluster);
|
List<String> subjects) {
|
||||||
return allSubjectNames
|
return Flux.fromIterable(subjects)
|
||||||
.flatMapMany(Flux::fromArray)
|
.concatMap(subject -> getLatestSchemaVersionBySubject(cluster, subject))
|
||||||
.flatMap(subject -> getLatestSchemaVersionBySubject(cluster, subject));
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mono<String[]> getAllSubjectNames(KafkaCluster cluster) {
|
public Mono<String[]> getAllSubjectNames(KafkaCluster cluster) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.provectus.kafka.ui;
|
||||||
import com.provectus.kafka.ui.model.CompatibilityLevelDTO;
|
import com.provectus.kafka.ui.model.CompatibilityLevelDTO;
|
||||||
import com.provectus.kafka.ui.model.NewSchemaSubjectDTO;
|
import com.provectus.kafka.ui.model.NewSchemaSubjectDTO;
|
||||||
import com.provectus.kafka.ui.model.SchemaSubjectDTO;
|
import com.provectus.kafka.ui.model.SchemaSubjectDTO;
|
||||||
|
import com.provectus.kafka.ui.model.SchemaSubjectsResponseDTO;
|
||||||
import com.provectus.kafka.ui.model.SchemaTypeDTO;
|
import com.provectus.kafka.ui.model.SchemaTypeDTO;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
@ -145,14 +146,14 @@ class SchemaRegistryServiceTests extends AbstractBaseTest {
|
||||||
.uri("/api/clusters/{clusterName}/schemas", LOCAL)
|
.uri("/api/clusters/{clusterName}/schemas", LOCAL)
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().isOk()
|
.expectStatus().isOk()
|
||||||
.expectBodyList(SchemaSubjectDTO.class)
|
.expectBody(SchemaSubjectsResponseDTO.class)
|
||||||
.consumeWith(result -> {
|
.consumeWith(result -> {
|
||||||
List<SchemaSubjectDTO> responseBody = result.getResponseBody();
|
SchemaSubjectsResponseDTO responseBody = result.getResponseBody();
|
||||||
log.info("Response of test schemas: {}", responseBody);
|
log.info("Response of test schemas: {}", responseBody);
|
||||||
Assertions.assertNotNull(responseBody);
|
Assertions.assertNotNull(responseBody);
|
||||||
Assertions.assertFalse(responseBody.isEmpty());
|
Assertions.assertFalse(responseBody.getSchemas().isEmpty());
|
||||||
|
|
||||||
SchemaSubjectDTO actualSchemaSubject = responseBody.stream()
|
SchemaSubjectDTO actualSchemaSubject = responseBody.getSchemas().stream()
|
||||||
.filter(schemaSubject -> subject.equals(schemaSubject.getSubject()))
|
.filter(schemaSubject -> subject.equals(schemaSubject.getSubject()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
package com.provectus.kafka.ui.service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyList;
|
||||||
|
import static org.mockito.ArgumentMatchers.isA;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.provectus.kafka.ui.controller.SchemasController;
|
||||||
|
import com.provectus.kafka.ui.model.InternalSchemaRegistry;
|
||||||
|
import com.provectus.kafka.ui.model.KafkaCluster;
|
||||||
|
import com.provectus.kafka.ui.model.SchemaSubjectDTO;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public class SchemaRegistryPaginationTest {
|
||||||
|
|
||||||
|
private static final String LOCAL_KAFKA_CLUSTER_NAME = "local";
|
||||||
|
|
||||||
|
private final SchemaRegistryService schemaRegistryService = mock(SchemaRegistryService.class);
|
||||||
|
private final ClustersStorage clustersStorage = mock(ClustersStorage.class);
|
||||||
|
|
||||||
|
private SchemasController controller;
|
||||||
|
|
||||||
|
private void init(String[] subjects) {
|
||||||
|
when(schemaRegistryService.getAllSubjectNames(isA(KafkaCluster.class)))
|
||||||
|
.thenReturn(Mono.just(subjects));
|
||||||
|
when(schemaRegistryService
|
||||||
|
.getAllLatestVersionSchemas(isA(KafkaCluster.class), anyList())).thenCallRealMethod();
|
||||||
|
when(clustersStorage.getClusterByName(isA(String.class)))
|
||||||
|
.thenReturn(Optional.of(buildKafkaCluster(LOCAL_KAFKA_CLUSTER_NAME)));
|
||||||
|
when(schemaRegistryService.getLatestSchemaVersionBySubject(isA(KafkaCluster.class), isA(String.class)))
|
||||||
|
.thenAnswer(a -> Mono.just(new SchemaSubjectDTO().subject(a.getArgument(1))));
|
||||||
|
this.controller = new SchemasController(schemaRegistryService);
|
||||||
|
this.controller.setClustersStorage(clustersStorage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldListFirst25andThen10Schemas() {
|
||||||
|
init(
|
||||||
|
IntStream.rangeClosed(1, 100)
|
||||||
|
.boxed()
|
||||||
|
.map(num -> "subject" + num)
|
||||||
|
.toArray(String[]::new)
|
||||||
|
);
|
||||||
|
var schemasFirst25 = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,
|
||||||
|
null, null, null, null).block();
|
||||||
|
assertThat(schemasFirst25.getBody().getPageCount()).isEqualTo(4);
|
||||||
|
assertThat(schemasFirst25.getBody().getSchemas()).hasSize(25);
|
||||||
|
assertThat(schemasFirst25.getBody().getSchemas())
|
||||||
|
.isSortedAccordingTo(Comparator.comparing(SchemaSubjectDTO::getSubject));
|
||||||
|
|
||||||
|
var schemasFirst10 = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,
|
||||||
|
null, 10, null, null).block();
|
||||||
|
|
||||||
|
assertThat(schemasFirst10.getBody().getPageCount()).isEqualTo(10);
|
||||||
|
assertThat(schemasFirst10.getBody().getSchemas()).hasSize(10);
|
||||||
|
assertThat(schemasFirst10.getBody().getSchemas())
|
||||||
|
.isSortedAccordingTo(Comparator.comparing(SchemaSubjectDTO::getSubject));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldListSchemasContaining_1() {
|
||||||
|
init(
|
||||||
|
IntStream.rangeClosed(1, 100)
|
||||||
|
.boxed()
|
||||||
|
.map(num -> "subject" + num)
|
||||||
|
.toArray(String[]::new)
|
||||||
|
);
|
||||||
|
var schemasSearch7 = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,
|
||||||
|
null, null, "1", null).block();
|
||||||
|
assertThat(schemasSearch7.getBody().getPageCount()).isEqualTo(1);
|
||||||
|
assertThat(schemasSearch7.getBody().getSchemas()).hasSize(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCorrectlyHandleNonPositivePageNumberAndPageSize() {
|
||||||
|
init(
|
||||||
|
IntStream.rangeClosed(1, 100)
|
||||||
|
.boxed()
|
||||||
|
.map(num -> "subject" + num)
|
||||||
|
.toArray(String[]::new)
|
||||||
|
);
|
||||||
|
var schemas = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,
|
||||||
|
0, -1, null, null).block();;
|
||||||
|
|
||||||
|
assertThat(schemas.getBody().getPageCount()).isEqualTo(4);
|
||||||
|
assertThat(schemas.getBody().getSchemas()).hasSize(25);
|
||||||
|
assertThat(schemas.getBody().getSchemas()).isSortedAccordingTo(Comparator.comparing(SchemaSubjectDTO::getSubject));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCalculateCorrectPageCountForNonDivisiblePageSize() {
|
||||||
|
init(
|
||||||
|
IntStream.rangeClosed(1, 100)
|
||||||
|
.boxed()
|
||||||
|
.map(num -> "subject" + num)
|
||||||
|
.toArray(String[]::new)
|
||||||
|
);
|
||||||
|
|
||||||
|
var schemas = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,
|
||||||
|
4, 33, null, null).block();
|
||||||
|
|
||||||
|
assertThat(schemas.getBody().getPageCount()).isEqualTo(4);
|
||||||
|
assertThat(schemas.getBody().getSchemas()).hasSize(1);
|
||||||
|
assertThat(schemas.getBody().getSchemas().get(0).getSubject()).isEqualTo("subject99");
|
||||||
|
}
|
||||||
|
|
||||||
|
private KafkaCluster buildKafkaCluster(String clusterName) {
|
||||||
|
return KafkaCluster.builder()
|
||||||
|
.name(clusterName)
|
||||||
|
.schemaRegistry(InternalSchemaRegistry.builder().build())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -790,15 +790,28 @@ paths:
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
- name: page
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- name: perPage
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- name: search
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: OK
|
description: OK
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: array
|
$ref: '#/components/schemas/SchemaSubjectsResponse'
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/SchemaSubject'
|
|
||||||
|
|
||||||
/api/clusters/{clusterName}/schemas/{subject}:
|
/api/clusters/{clusterName}/schemas/{subject}:
|
||||||
delete:
|
delete:
|
||||||
|
@ -2248,6 +2261,16 @@ components:
|
||||||
required:
|
required:
|
||||||
- isCompatible
|
- isCompatible
|
||||||
|
|
||||||
|
SchemaSubjectsResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pageCount:
|
||||||
|
type: integer
|
||||||
|
schemas:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/SchemaSubject'
|
||||||
|
|
||||||
Connect:
|
Connect:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
|
@ -13,15 +13,19 @@ import { Table } from 'components/common/table/Table/Table.styled';
|
||||||
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
||||||
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
||||||
import {
|
import {
|
||||||
|
fetchLatestSchema,
|
||||||
fetchSchemaVersions,
|
fetchSchemaVersions,
|
||||||
getAreSchemasFulfilled,
|
getAreSchemaLatestFulfilled,
|
||||||
getAreSchemaVersionsFulfilled,
|
getAreSchemaVersionsFulfilled,
|
||||||
schemasApiClient,
|
schemasApiClient,
|
||||||
|
SCHEMAS_VERSIONS_FETCH_ACTION,
|
||||||
|
SCHEMA_LATEST_FETCH_ACTION,
|
||||||
selectAllSchemaVersions,
|
selectAllSchemaVersions,
|
||||||
selectSchemaById,
|
getSchemaLatest,
|
||||||
} from 'redux/reducers/schemas/schemasSlice';
|
} from 'redux/reducers/schemas/schemasSlice';
|
||||||
import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
|
import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
|
||||||
import { getResponse } from 'lib/errorHandling';
|
import { getResponse } from 'lib/errorHandling';
|
||||||
|
import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
|
||||||
|
|
||||||
import LatestVersionItem from './LatestVersion/LatestVersionItem';
|
import LatestVersionItem from './LatestVersion/LatestVersionItem';
|
||||||
import SchemaVersion from './SchemaVersion/SchemaVersion';
|
import SchemaVersion from './SchemaVersion/SchemaVersion';
|
||||||
|
@ -39,15 +43,23 @@ const Details: React.FC = () => {
|
||||||
] = React.useState(false);
|
] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
dispatch(fetchSchemaVersions({ clusterName, subject }));
|
dispatch(fetchLatestSchema({ clusterName, subject }));
|
||||||
|
return () => {
|
||||||
|
dispatch(resetLoaderById(SCHEMA_LATEST_FETCH_ACTION));
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const areSchemasFetched = useAppSelector(getAreSchemasFulfilled);
|
React.useEffect(() => {
|
||||||
|
dispatch(fetchSchemaVersions({ clusterName, subject }));
|
||||||
|
return () => {
|
||||||
|
dispatch(resetLoaderById(SCHEMAS_VERSIONS_FETCH_ACTION));
|
||||||
|
};
|
||||||
|
}, [clusterName, subject]);
|
||||||
|
|
||||||
|
const versions = useAppSelector((state) => selectAllSchemaVersions(state));
|
||||||
|
const schema = useAppSelector(getSchemaLatest);
|
||||||
|
const isFetched = useAppSelector(getAreSchemaLatestFulfilled);
|
||||||
const areVersionsFetched = useAppSelector(getAreSchemaVersionsFulfilled);
|
const areVersionsFetched = useAppSelector(getAreSchemaVersionsFulfilled);
|
||||||
const schema = useAppSelector((state) => selectSchemaById(state, subject));
|
|
||||||
const versions = useAppSelector((state) =>
|
|
||||||
selectAllSchemaVersions(state).filter((v) => v.subject === subject)
|
|
||||||
);
|
|
||||||
|
|
||||||
const onDelete = React.useCallback(async () => {
|
const onDelete = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -62,7 +74,7 @@ const Details: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [clusterName, subject]);
|
}, [clusterName, subject]);
|
||||||
|
|
||||||
if (!areSchemasFetched || !schema) {
|
if (!isFetched || !schema) {
|
||||||
return <PageLoader />;
|
return <PageLoader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,111 +5,119 @@ import { Route } from 'react-router';
|
||||||
import { clusterSchemaPath } from 'lib/paths';
|
import { clusterSchemaPath } from 'lib/paths';
|
||||||
import { screen, waitFor } from '@testing-library/dom';
|
import { screen, waitFor } from '@testing-library/dom';
|
||||||
import {
|
import {
|
||||||
schemasFulfilledState,
|
schemasInitialState,
|
||||||
schemaVersion,
|
schemaVersion,
|
||||||
} from 'redux/reducers/schemas/__test__/fixtures';
|
} from 'redux/reducers/schemas/__test__/fixtures';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
|
import ClusterContext, {
|
||||||
|
ContextProps,
|
||||||
|
initialValue as contextInitialValue,
|
||||||
|
} from 'components/contexts/ClusterContext';
|
||||||
|
import { RootState } from 'redux/interfaces';
|
||||||
|
|
||||||
|
import { versionPayload, versionEmptyPayload } from './fixtures';
|
||||||
|
|
||||||
const clusterName = 'testClusterName';
|
const clusterName = 'testClusterName';
|
||||||
const subject = 'schema7_1';
|
const schemasAPILatestUrl = `/api/clusters/${clusterName}/schemas/${schemaVersion.subject}/latest`;
|
||||||
|
const schemasAPIVersionsUrl = `/api/clusters/${clusterName}/schemas/${schemaVersion.subject}/versions`;
|
||||||
|
|
||||||
|
const renderComponent = (
|
||||||
|
initialState: RootState['schemas'] = schemasInitialState,
|
||||||
|
context: ContextProps = contextInitialValue
|
||||||
|
) => {
|
||||||
|
return render(
|
||||||
|
<Route path={clusterSchemaPath(':clusterName', ':subject')}>
|
||||||
|
<ClusterContext.Provider value={context}>
|
||||||
|
<Details />
|
||||||
|
</ClusterContext.Provider>
|
||||||
|
</Route>,
|
||||||
|
{
|
||||||
|
pathname: clusterSchemaPath(clusterName, schemaVersion.subject),
|
||||||
|
preloadedState: {
|
||||||
|
schemas: initialState,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
describe('Details', () => {
|
describe('Details', () => {
|
||||||
describe('for an initial state', () => {
|
afterEach(() => fetchMock.reset());
|
||||||
it('renders pageloader', () => {
|
|
||||||
render(
|
describe('fetch failed', () => {
|
||||||
<Route path={clusterSchemaPath(':clusterName', ':subject')}>
|
beforeEach(async () => {
|
||||||
<Details />
|
const schemasAPILatestMock = fetchMock.getOnce(schemasAPILatestUrl, 404);
|
||||||
</Route>,
|
const schemasAPIVersionsMock = fetchMock.getOnce(
|
||||||
{
|
schemasAPIVersionsUrl,
|
||||||
pathname: clusterSchemaPath(clusterName, subject),
|
404
|
||||||
preloadedState: {},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
renderComponent();
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(schemasAPILatestMock.called()).toBeTruthy();
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(schemasAPIVersionsMock.called()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders pageloader', () => {
|
||||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||||
expect(screen.queryByText(subject)).not.toBeInTheDocument();
|
expect(screen.queryByText(schemaVersion.subject)).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText('Edit Schema')).not.toBeInTheDocument();
|
expect(screen.queryByText('Edit Schema')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText('Remove Schema')).not.toBeInTheDocument();
|
expect(screen.queryByText('Remove Schema')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('for a loaded scheme', () => {
|
describe('fetch success', () => {
|
||||||
beforeEach(() => {
|
describe('has schema versions', () => {
|
||||||
render(
|
beforeEach(async () => {
|
||||||
<Route path={clusterSchemaPath(':clusterName', ':subject')}>
|
const schemasAPILatestMock = fetchMock.getOnce(
|
||||||
<Details />
|
schemasAPILatestUrl,
|
||||||
</Route>,
|
schemaVersion
|
||||||
{
|
);
|
||||||
pathname: clusterSchemaPath(clusterName, subject),
|
const schemasAPIVersionsMock = fetchMock.getOnce(
|
||||||
preloadedState: {
|
schemasAPIVersionsUrl,
|
||||||
loader: {
|
versionPayload
|
||||||
'schemas/fetch': 'fulfilled',
|
);
|
||||||
},
|
renderComponent();
|
||||||
schemas: schemasFulfilledState,
|
await waitFor(() => {
|
||||||
},
|
expect(schemasAPILatestMock.called()).toBeTruthy();
|
||||||
}
|
});
|
||||||
);
|
await waitFor(() => {
|
||||||
|
expect(schemasAPIVersionsMock.called()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders component with schema info', () => {
|
||||||
|
expect(screen.getByText('Edit Schema')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders component with shema info', () => {
|
describe('empty schema versions', () => {
|
||||||
expect(screen.getByText('Edit Schema')).toBeInTheDocument();
|
beforeEach(async () => {
|
||||||
});
|
const schemasAPILatestMock = fetchMock.getOnce(
|
||||||
|
schemasAPILatestUrl,
|
||||||
|
schemaVersion
|
||||||
|
);
|
||||||
|
const schemasAPIVersionsMock = fetchMock.getOnce(
|
||||||
|
schemasAPIVersionsUrl,
|
||||||
|
versionEmptyPayload
|
||||||
|
);
|
||||||
|
renderComponent();
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(schemasAPILatestMock.called()).toBeTruthy();
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(schemasAPIVersionsMock.called()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('renders progressbar for versions block', () => {
|
// seems like incorrect behaviour
|
||||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
it('renders versions table with 0 items', () => {
|
||||||
expect(screen.queryByRole('table')).not.toBeInTheDocument();
|
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||||
});
|
expect(screen.getByText('No active Schema')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('for a loaded scheme and versions', () => {
|
|
||||||
afterEach(() => fetchMock.restore());
|
|
||||||
it('renders versions table', async () => {
|
|
||||||
const mock = fetchMock.getOnce(
|
|
||||||
`/api/clusters/${clusterName}/schemas/${subject}/versions`,
|
|
||||||
[schemaVersion]
|
|
||||||
);
|
|
||||||
render(
|
|
||||||
<Route path={clusterSchemaPath(':clusterName', ':subject')}>
|
|
||||||
<Details />
|
|
||||||
</Route>,
|
|
||||||
{
|
|
||||||
pathname: clusterSchemaPath(clusterName, subject),
|
|
||||||
preloadedState: {
|
|
||||||
loader: {
|
|
||||||
'schemas/fetch': 'fulfilled',
|
|
||||||
},
|
|
||||||
schemas: schemasFulfilledState,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
await waitFor(() => expect(mock.called()).toBeTruthy());
|
|
||||||
|
|
||||||
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders versions table with 0 items', async () => {
|
|
||||||
const mock = fetchMock.getOnce(
|
|
||||||
`/api/clusters/${clusterName}/schemas/${subject}/versions`,
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
render(
|
|
||||||
<Route path={clusterSchemaPath(':clusterName', ':subject')}>
|
|
||||||
<Details />
|
|
||||||
</Route>,
|
|
||||||
{
|
|
||||||
pathname: clusterSchemaPath(clusterName, subject),
|
|
||||||
preloadedState: {
|
|
||||||
loader: {
|
|
||||||
'schemas/fetch': 'fulfilled',
|
|
||||||
},
|
|
||||||
schemas: schemasFulfilledState,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
await waitFor(() => expect(mock.called()).toBeTruthy());
|
|
||||||
|
|
||||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('No active Schema')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,47 +1,36 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount, shallow } from 'enzyme';
|
|
||||||
import LatestVersionItem from 'components/Schemas/Details/LatestVersion/LatestVersionItem';
|
import LatestVersionItem from 'components/Schemas/Details/LatestVersion/LatestVersionItem';
|
||||||
import { ThemeProvider } from 'styled-components';
|
import { SchemaSubject } from 'generated-sources';
|
||||||
import theme from 'theme/theme';
|
import { render } from 'lib/testHelpers';
|
||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
|
||||||
import { jsonSchema, protoSchema } from './fixtures';
|
import { jsonSchema, protoSchema } from './fixtures';
|
||||||
|
|
||||||
|
const renderComponent = (schema: SchemaSubject) => {
|
||||||
|
render(<LatestVersionItem schema={schema} />);
|
||||||
|
};
|
||||||
|
|
||||||
describe('LatestVersionItem', () => {
|
describe('LatestVersionItem', () => {
|
||||||
it('renders latest version of json schema', () => {
|
it('renders latest version of json schema', () => {
|
||||||
const wrapper = mount(
|
renderComponent(jsonSchema);
|
||||||
<ThemeProvider theme={theme}>
|
expect(screen.getByText('Relevant version')).toBeInTheDocument();
|
||||||
<LatestVersionItem schema={jsonSchema} />
|
expect(screen.getByText('Latest version')).toBeInTheDocument();
|
||||||
</ThemeProvider>
|
expect(screen.getByText('ID')).toBeInTheDocument();
|
||||||
);
|
expect(screen.getByText('Subject')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Compatibility')).toBeInTheDocument();
|
||||||
expect(wrapper.find('div[data-testid="meta-data"]').length).toEqual(1);
|
expect(screen.getByText('15')).toBeInTheDocument();
|
||||||
expect(
|
expect(screen.getByTestId('json-viewer')).toBeInTheDocument();
|
||||||
wrapper.find('div[data-testid="meta-data"] > div:first-child > p').text()
|
|
||||||
).toEqual('1');
|
|
||||||
expect(wrapper.exists('EditorViewer')).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders latest version of compatibility', () => {
|
it('renders latest version of compatibility', () => {
|
||||||
const wrapper = mount(
|
renderComponent(protoSchema);
|
||||||
<ThemeProvider theme={theme}>
|
expect(screen.getByText('Relevant version')).toBeInTheDocument();
|
||||||
<LatestVersionItem schema={protoSchema} />
|
expect(screen.getByText('Latest version')).toBeInTheDocument();
|
||||||
</ThemeProvider>
|
expect(screen.getByText('ID')).toBeInTheDocument();
|
||||||
);
|
expect(screen.getByText('Subject')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Compatibility')).toBeInTheDocument();
|
||||||
|
|
||||||
expect(wrapper.find('div[data-testid="meta-data"]').length).toEqual(1);
|
expect(screen.getByText('BACKWARD')).toBeInTheDocument();
|
||||||
expect(
|
expect(screen.getByTestId('json-viewer')).toBeInTheDocument();
|
||||||
wrapper.find('div[data-testid="meta-data"] > div:last-child > p').text()
|
|
||||||
).toEqual('BACKWARD');
|
|
||||||
expect(wrapper.exists('EditorViewer')).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('matches snapshot', () => {
|
|
||||||
expect(
|
|
||||||
shallow(
|
|
||||||
<ThemeProvider theme={theme}>
|
|
||||||
<LatestVersionItem schema={jsonSchema} />
|
|
||||||
</ThemeProvider>
|
|
||||||
)
|
|
||||||
).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,36 +1,26 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow, mount } from 'enzyme';
|
|
||||||
import SchemaVersion from 'components/Schemas/Details/SchemaVersion/SchemaVersion';
|
import SchemaVersion from 'components/Schemas/Details/SchemaVersion/SchemaVersion';
|
||||||
import { ThemeProvider } from 'styled-components';
|
import { render } from 'lib/testHelpers';
|
||||||
import theme from 'theme/theme';
|
import { screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
import { versions } from './fixtures';
|
import { versions } from './fixtures';
|
||||||
|
|
||||||
|
const renderComponent = () => {
|
||||||
|
render(
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<SchemaVersion version={versions[0]} />
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
describe('SchemaVersion', () => {
|
describe('SchemaVersion', () => {
|
||||||
it('renders versions', () => {
|
it('renders versions', () => {
|
||||||
const wrapper = mount(
|
renderComponent();
|
||||||
<ThemeProvider theme={theme}>
|
expect(screen.getAllByRole('cell')).toHaveLength(3);
|
||||||
<table>
|
expect(screen.queryByTestId('json-viewer')).not.toBeInTheDocument();
|
||||||
<tbody>
|
userEvent.click(screen.getByRole('button'));
|
||||||
<SchemaVersion version={versions[0]} />
|
expect(screen.getByTestId('json-viewer')).toBeInTheDocument();
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(wrapper.find('td').length).toEqual(3);
|
|
||||||
expect(wrapper.exists('Editor')).toBeFalsy();
|
|
||||||
wrapper.find('span').simulate('click');
|
|
||||||
expect(wrapper.exists('Editor')).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('matches snapshot', () => {
|
|
||||||
expect(
|
|
||||||
shallow(
|
|
||||||
<ThemeProvider theme={theme}>
|
|
||||||
<SchemaVersion version={versions[0]} />
|
|
||||||
</ThemeProvider>
|
|
||||||
)
|
|
||||||
).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,368 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`LatestVersionItem matches snapshot 1`] = `
|
|
||||||
<ContextProvider
|
|
||||||
value={
|
|
||||||
Object {
|
|
||||||
"alert": Object {
|
|
||||||
"color": Object {
|
|
||||||
"error": "#FAD1D1",
|
|
||||||
"info": "#E3E6E8",
|
|
||||||
"success": "#D6F5E0",
|
|
||||||
"warning": "#FFEECC",
|
|
||||||
},
|
|
||||||
"shadow": "rgba(0, 0, 0, 0.1)",
|
|
||||||
},
|
|
||||||
"breadcrumb": "#ABB5BA",
|
|
||||||
"button": Object {
|
|
||||||
"border": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"hover": "#454F54",
|
|
||||||
"normal": "#73848C",
|
|
||||||
},
|
|
||||||
"fontSize": Object {
|
|
||||||
"L": "16px",
|
|
||||||
"M": "14px",
|
|
||||||
"S": "14px",
|
|
||||||
},
|
|
||||||
"height": Object {
|
|
||||||
"L": "40px",
|
|
||||||
"M": "32px",
|
|
||||||
"S": "24px",
|
|
||||||
},
|
|
||||||
"primary": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"active": "#1414B8",
|
|
||||||
"hover": "#1717CF",
|
|
||||||
"normal": "#4F4FFF",
|
|
||||||
},
|
|
||||||
"color": "#FFFFFF",
|
|
||||||
"invertedColors": Object {
|
|
||||||
"active": "#1414B8",
|
|
||||||
"hover": "#1717CF",
|
|
||||||
"normal": "#4F4FFF",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"secondary": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"active": "#D5DADD",
|
|
||||||
"hover": "#E3E6E8",
|
|
||||||
"normal": "#F1F2F3",
|
|
||||||
},
|
|
||||||
"color": "#171A1C",
|
|
||||||
"invertedColors": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"hover": "#454F54",
|
|
||||||
"normal": "#73848C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"circularAlert": Object {
|
|
||||||
"color": Object {
|
|
||||||
"error": "#E51A1A",
|
|
||||||
"info": "#E3E6E8",
|
|
||||||
"success": "#5CD685",
|
|
||||||
"warning": "#FFEECC",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"configList": Object {
|
|
||||||
"color": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"connectEditWarning": "#FFEECC",
|
|
||||||
"consumerTopicContent": Object {
|
|
||||||
"backgroundColor": "#F1F2F3",
|
|
||||||
},
|
|
||||||
"dangerZone": Object {
|
|
||||||
"borderColor": "#E3E6E8",
|
|
||||||
"color": "#E51A1A",
|
|
||||||
},
|
|
||||||
"dropdown": Object {
|
|
||||||
"color": "#E51A1A",
|
|
||||||
},
|
|
||||||
"heading": Object {
|
|
||||||
"h1": Object {
|
|
||||||
"color": "#171A1C",
|
|
||||||
},
|
|
||||||
"h3": Object {
|
|
||||||
"color": "#73848C",
|
|
||||||
"fontSize": "14px",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"icons": Object {
|
|
||||||
"closeIcon": "#ABB5BA",
|
|
||||||
"liveIcon": Object {
|
|
||||||
"circleBig": "#FAD1D1",
|
|
||||||
"circleSmall": "#E51A1A",
|
|
||||||
},
|
|
||||||
"messageToggleIconClosed": "#ABB5BA",
|
|
||||||
"messageToggleIconOpened": "#171A1C",
|
|
||||||
"verticalElipsisIcon": "#73848C",
|
|
||||||
},
|
|
||||||
"input": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"readOnly": "#F1F2F3",
|
|
||||||
},
|
|
||||||
"borderColor": Object {
|
|
||||||
"disabled": "#E3E6E8",
|
|
||||||
"focus": "#454F54",
|
|
||||||
"hover": "#73848C",
|
|
||||||
"normal": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"disabled": "#ABB5BA",
|
|
||||||
"placeholder": Object {
|
|
||||||
"normal": "#ABB5BA",
|
|
||||||
"readOnly": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"readOnly": "#171A1C",
|
|
||||||
},
|
|
||||||
"error": "#E51A1A",
|
|
||||||
"icon": Object {
|
|
||||||
"color": "#454F54",
|
|
||||||
},
|
|
||||||
"label": Object {
|
|
||||||
"color": "#454F54",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"layout": Object {
|
|
||||||
"minWidth": "1200px",
|
|
||||||
"navBarHeight": "3.25rem",
|
|
||||||
"navBarWidth": "201px",
|
|
||||||
"overlay": Object {
|
|
||||||
"backgroundColor": "#73848C",
|
|
||||||
},
|
|
||||||
"stuffBorderColor": "#E3E6E8",
|
|
||||||
"stuffColor": "#F1F2F3",
|
|
||||||
},
|
|
||||||
"menu": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"active": "#E3E6E8",
|
|
||||||
"hover": "#F1F2F3",
|
|
||||||
"normal": "#FFFFFF",
|
|
||||||
},
|
|
||||||
"chevronIconColor": "#73848C",
|
|
||||||
"color": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"hover": "#73848C",
|
|
||||||
"normal": "#73848C",
|
|
||||||
},
|
|
||||||
"statusIconColor": Object {
|
|
||||||
"offline": "#E51A1A",
|
|
||||||
"online": "#5CD685",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"metrics": Object {
|
|
||||||
"backgroundColor": "#F1F2F3",
|
|
||||||
"filters": Object {
|
|
||||||
"color": Object {
|
|
||||||
"icon": "#171A1C",
|
|
||||||
"normal": "#73848C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"indicator": Object {
|
|
||||||
"backgroundColor": "#FFFFFF",
|
|
||||||
"lightTextColor": "#ABB5BA",
|
|
||||||
"titleColor": "#73848C",
|
|
||||||
"warningTextColor": "#E51A1A",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"modal": Object {
|
|
||||||
"backgroundColor": "#FFFFFF",
|
|
||||||
"border": Object {
|
|
||||||
"bottom": "#F1F2F3",
|
|
||||||
"top": "#F1F2F3",
|
|
||||||
},
|
|
||||||
"overlay": "rgba(10, 10, 10, 0.1)",
|
|
||||||
"shadow": "rgba(0, 0, 0, 0.1)",
|
|
||||||
},
|
|
||||||
"pageLoader": Object {
|
|
||||||
"borderBottomColor": "#FFFFFF",
|
|
||||||
"borderColor": "#4F4FFF",
|
|
||||||
},
|
|
||||||
"pagination": Object {
|
|
||||||
"backgroundColor": "#FFFFFF",
|
|
||||||
"borderColor": Object {
|
|
||||||
"active": "#454F54",
|
|
||||||
"disabled": "#C7CED1",
|
|
||||||
"hover": "#73848C",
|
|
||||||
"normal": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"disabled": "#C7CED1",
|
|
||||||
"hover": "#171A1C",
|
|
||||||
"normal": "#171A1C",
|
|
||||||
},
|
|
||||||
"currentPage": "#E3E6E8",
|
|
||||||
},
|
|
||||||
"panelColor": "#FFFFFF",
|
|
||||||
"primaryTab": Object {
|
|
||||||
"borderColor": Object {
|
|
||||||
"active": "#4F4FFF",
|
|
||||||
"hover": "transparent",
|
|
||||||
"nav": "#E3E6E8",
|
|
||||||
"normal": "transparent",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"hover": "#171A1C",
|
|
||||||
"normal": "#73848C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"schema": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"div": "#FFFFFF",
|
|
||||||
"tr": "#F1F2F3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"scrollbar": Object {
|
|
||||||
"thumbColor": Object {
|
|
||||||
"active": "#73848C",
|
|
||||||
"normal": "#FFFFFF",
|
|
||||||
},
|
|
||||||
"trackColor": Object {
|
|
||||||
"active": "#F1F2F3",
|
|
||||||
"normal": "#FFFFFF",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"secondaryTab": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"active": "#E3E6E8",
|
|
||||||
"hover": "#F1F2F3",
|
|
||||||
"normal": "#FFFFFF",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"hover": "#171A1C",
|
|
||||||
"normal": "#73848C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"select": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"active": "#E3E6E8",
|
|
||||||
"hover": "#E3E6E8",
|
|
||||||
"normal": "#FFFFFF",
|
|
||||||
},
|
|
||||||
"borderColor": Object {
|
|
||||||
"active": "#454F54",
|
|
||||||
"disabled": "#E3E6E8",
|
|
||||||
"hover": "#73848C",
|
|
||||||
"normal": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"disabled": "#ABB5BA",
|
|
||||||
"hover": "#171A1C",
|
|
||||||
"normal": "#171A1C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"switch": Object {
|
|
||||||
"checked": "#29A352",
|
|
||||||
"circle": "#FFFFFF",
|
|
||||||
"unchecked": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"table": Object {
|
|
||||||
"link": Object {
|
|
||||||
"color": Object {
|
|
||||||
"normal": "#171A1C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"td": Object {
|
|
||||||
"color": Object {
|
|
||||||
"normal": "#171A1C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"th": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"normal": "#FFFFFF",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"active": "#4F4FFF",
|
|
||||||
"hover": "#4F4FFF",
|
|
||||||
"normal": "#73848C",
|
|
||||||
},
|
|
||||||
"previewColor": Object {
|
|
||||||
"normal": "#4F4FFF",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"tr": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"hover": "#F1F2F3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"tag": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"blue": "#e3f2fd",
|
|
||||||
"gray": "#F1F2F3",
|
|
||||||
"green": "#D6F5E0",
|
|
||||||
"red": "#FAD1D1",
|
|
||||||
"white": "#E3E6E8",
|
|
||||||
"yellow": "#FFEECC",
|
|
||||||
},
|
|
||||||
"color": "#171A1C",
|
|
||||||
},
|
|
||||||
"textArea": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"readOnly": "#F1F2F3",
|
|
||||||
},
|
|
||||||
"borderColor": Object {
|
|
||||||
"disabled": "#E3E6E8",
|
|
||||||
"focus": "#454F54",
|
|
||||||
"hover": "#73848C",
|
|
||||||
"normal": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"disabled": "#ABB5BA",
|
|
||||||
"placeholder": Object {
|
|
||||||
"focus": Object {
|
|
||||||
"normal": "transparent",
|
|
||||||
"readOnly": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"normal": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"readOnly": "#171A1C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"topicFormLabel": Object {
|
|
||||||
"color": "#73848C",
|
|
||||||
},
|
|
||||||
"topicMetaData": Object {
|
|
||||||
"backgroundColor": "#F1F2F3",
|
|
||||||
"color": Object {
|
|
||||||
"label": "#73848C",
|
|
||||||
"meta": "#ABB5BA",
|
|
||||||
"value": "#2F3639",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"topicsList": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"active": "#E3E6E8",
|
|
||||||
"hover": "#F1F2F3",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"hover": "#73848C",
|
|
||||||
"normal": "#171A1C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"viewer": Object {
|
|
||||||
"wrapper": "#f9fafa",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<LatestVersionItem
|
|
||||||
schema={
|
|
||||||
Object {
|
|
||||||
"compatibilityLevel": "BACKWARD",
|
|
||||||
"id": 1,
|
|
||||||
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
|
|
||||||
"schemaType": "JSON",
|
|
||||||
"subject": "test",
|
|
||||||
"version": "1",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ContextProvider>
|
|
||||||
`;
|
|
|
@ -1,368 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`SchemaVersion matches snapshot 1`] = `
|
|
||||||
<ContextProvider
|
|
||||||
value={
|
|
||||||
Object {
|
|
||||||
"alert": Object {
|
|
||||||
"color": Object {
|
|
||||||
"error": "#FAD1D1",
|
|
||||||
"info": "#E3E6E8",
|
|
||||||
"success": "#D6F5E0",
|
|
||||||
"warning": "#FFEECC",
|
|
||||||
},
|
|
||||||
"shadow": "rgba(0, 0, 0, 0.1)",
|
|
||||||
},
|
|
||||||
"breadcrumb": "#ABB5BA",
|
|
||||||
"button": Object {
|
|
||||||
"border": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"hover": "#454F54",
|
|
||||||
"normal": "#73848C",
|
|
||||||
},
|
|
||||||
"fontSize": Object {
|
|
||||||
"L": "16px",
|
|
||||||
"M": "14px",
|
|
||||||
"S": "14px",
|
|
||||||
},
|
|
||||||
"height": Object {
|
|
||||||
"L": "40px",
|
|
||||||
"M": "32px",
|
|
||||||
"S": "24px",
|
|
||||||
},
|
|
||||||
"primary": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"active": "#1414B8",
|
|
||||||
"hover": "#1717CF",
|
|
||||||
"normal": "#4F4FFF",
|
|
||||||
},
|
|
||||||
"color": "#FFFFFF",
|
|
||||||
"invertedColors": Object {
|
|
||||||
"active": "#1414B8",
|
|
||||||
"hover": "#1717CF",
|
|
||||||
"normal": "#4F4FFF",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"secondary": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"active": "#D5DADD",
|
|
||||||
"hover": "#E3E6E8",
|
|
||||||
"normal": "#F1F2F3",
|
|
||||||
},
|
|
||||||
"color": "#171A1C",
|
|
||||||
"invertedColors": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"hover": "#454F54",
|
|
||||||
"normal": "#73848C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"circularAlert": Object {
|
|
||||||
"color": Object {
|
|
||||||
"error": "#E51A1A",
|
|
||||||
"info": "#E3E6E8",
|
|
||||||
"success": "#5CD685",
|
|
||||||
"warning": "#FFEECC",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"configList": Object {
|
|
||||||
"color": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"connectEditWarning": "#FFEECC",
|
|
||||||
"consumerTopicContent": Object {
|
|
||||||
"backgroundColor": "#F1F2F3",
|
|
||||||
},
|
|
||||||
"dangerZone": Object {
|
|
||||||
"borderColor": "#E3E6E8",
|
|
||||||
"color": "#E51A1A",
|
|
||||||
},
|
|
||||||
"dropdown": Object {
|
|
||||||
"color": "#E51A1A",
|
|
||||||
},
|
|
||||||
"heading": Object {
|
|
||||||
"h1": Object {
|
|
||||||
"color": "#171A1C",
|
|
||||||
},
|
|
||||||
"h3": Object {
|
|
||||||
"color": "#73848C",
|
|
||||||
"fontSize": "14px",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"icons": Object {
|
|
||||||
"closeIcon": "#ABB5BA",
|
|
||||||
"liveIcon": Object {
|
|
||||||
"circleBig": "#FAD1D1",
|
|
||||||
"circleSmall": "#E51A1A",
|
|
||||||
},
|
|
||||||
"messageToggleIconClosed": "#ABB5BA",
|
|
||||||
"messageToggleIconOpened": "#171A1C",
|
|
||||||
"verticalElipsisIcon": "#73848C",
|
|
||||||
},
|
|
||||||
"input": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"readOnly": "#F1F2F3",
|
|
||||||
},
|
|
||||||
"borderColor": Object {
|
|
||||||
"disabled": "#E3E6E8",
|
|
||||||
"focus": "#454F54",
|
|
||||||
"hover": "#73848C",
|
|
||||||
"normal": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"disabled": "#ABB5BA",
|
|
||||||
"placeholder": Object {
|
|
||||||
"normal": "#ABB5BA",
|
|
||||||
"readOnly": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"readOnly": "#171A1C",
|
|
||||||
},
|
|
||||||
"error": "#E51A1A",
|
|
||||||
"icon": Object {
|
|
||||||
"color": "#454F54",
|
|
||||||
},
|
|
||||||
"label": Object {
|
|
||||||
"color": "#454F54",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"layout": Object {
|
|
||||||
"minWidth": "1200px",
|
|
||||||
"navBarHeight": "3.25rem",
|
|
||||||
"navBarWidth": "201px",
|
|
||||||
"overlay": Object {
|
|
||||||
"backgroundColor": "#73848C",
|
|
||||||
},
|
|
||||||
"stuffBorderColor": "#E3E6E8",
|
|
||||||
"stuffColor": "#F1F2F3",
|
|
||||||
},
|
|
||||||
"menu": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"active": "#E3E6E8",
|
|
||||||
"hover": "#F1F2F3",
|
|
||||||
"normal": "#FFFFFF",
|
|
||||||
},
|
|
||||||
"chevronIconColor": "#73848C",
|
|
||||||
"color": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"hover": "#73848C",
|
|
||||||
"normal": "#73848C",
|
|
||||||
},
|
|
||||||
"statusIconColor": Object {
|
|
||||||
"offline": "#E51A1A",
|
|
||||||
"online": "#5CD685",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"metrics": Object {
|
|
||||||
"backgroundColor": "#F1F2F3",
|
|
||||||
"filters": Object {
|
|
||||||
"color": Object {
|
|
||||||
"icon": "#171A1C",
|
|
||||||
"normal": "#73848C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"indicator": Object {
|
|
||||||
"backgroundColor": "#FFFFFF",
|
|
||||||
"lightTextColor": "#ABB5BA",
|
|
||||||
"titleColor": "#73848C",
|
|
||||||
"warningTextColor": "#E51A1A",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"modal": Object {
|
|
||||||
"backgroundColor": "#FFFFFF",
|
|
||||||
"border": Object {
|
|
||||||
"bottom": "#F1F2F3",
|
|
||||||
"top": "#F1F2F3",
|
|
||||||
},
|
|
||||||
"overlay": "rgba(10, 10, 10, 0.1)",
|
|
||||||
"shadow": "rgba(0, 0, 0, 0.1)",
|
|
||||||
},
|
|
||||||
"pageLoader": Object {
|
|
||||||
"borderBottomColor": "#FFFFFF",
|
|
||||||
"borderColor": "#4F4FFF",
|
|
||||||
},
|
|
||||||
"pagination": Object {
|
|
||||||
"backgroundColor": "#FFFFFF",
|
|
||||||
"borderColor": Object {
|
|
||||||
"active": "#454F54",
|
|
||||||
"disabled": "#C7CED1",
|
|
||||||
"hover": "#73848C",
|
|
||||||
"normal": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"disabled": "#C7CED1",
|
|
||||||
"hover": "#171A1C",
|
|
||||||
"normal": "#171A1C",
|
|
||||||
},
|
|
||||||
"currentPage": "#E3E6E8",
|
|
||||||
},
|
|
||||||
"panelColor": "#FFFFFF",
|
|
||||||
"primaryTab": Object {
|
|
||||||
"borderColor": Object {
|
|
||||||
"active": "#4F4FFF",
|
|
||||||
"hover": "transparent",
|
|
||||||
"nav": "#E3E6E8",
|
|
||||||
"normal": "transparent",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"hover": "#171A1C",
|
|
||||||
"normal": "#73848C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"schema": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"div": "#FFFFFF",
|
|
||||||
"tr": "#F1F2F3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"scrollbar": Object {
|
|
||||||
"thumbColor": Object {
|
|
||||||
"active": "#73848C",
|
|
||||||
"normal": "#FFFFFF",
|
|
||||||
},
|
|
||||||
"trackColor": Object {
|
|
||||||
"active": "#F1F2F3",
|
|
||||||
"normal": "#FFFFFF",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"secondaryTab": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"active": "#E3E6E8",
|
|
||||||
"hover": "#F1F2F3",
|
|
||||||
"normal": "#FFFFFF",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"hover": "#171A1C",
|
|
||||||
"normal": "#73848C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"select": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"active": "#E3E6E8",
|
|
||||||
"hover": "#E3E6E8",
|
|
||||||
"normal": "#FFFFFF",
|
|
||||||
},
|
|
||||||
"borderColor": Object {
|
|
||||||
"active": "#454F54",
|
|
||||||
"disabled": "#E3E6E8",
|
|
||||||
"hover": "#73848C",
|
|
||||||
"normal": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"disabled": "#ABB5BA",
|
|
||||||
"hover": "#171A1C",
|
|
||||||
"normal": "#171A1C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"switch": Object {
|
|
||||||
"checked": "#29A352",
|
|
||||||
"circle": "#FFFFFF",
|
|
||||||
"unchecked": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"table": Object {
|
|
||||||
"link": Object {
|
|
||||||
"color": Object {
|
|
||||||
"normal": "#171A1C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"td": Object {
|
|
||||||
"color": Object {
|
|
||||||
"normal": "#171A1C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"th": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"normal": "#FFFFFF",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"active": "#4F4FFF",
|
|
||||||
"hover": "#4F4FFF",
|
|
||||||
"normal": "#73848C",
|
|
||||||
},
|
|
||||||
"previewColor": Object {
|
|
||||||
"normal": "#4F4FFF",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"tr": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"hover": "#F1F2F3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"tag": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"blue": "#e3f2fd",
|
|
||||||
"gray": "#F1F2F3",
|
|
||||||
"green": "#D6F5E0",
|
|
||||||
"red": "#FAD1D1",
|
|
||||||
"white": "#E3E6E8",
|
|
||||||
"yellow": "#FFEECC",
|
|
||||||
},
|
|
||||||
"color": "#171A1C",
|
|
||||||
},
|
|
||||||
"textArea": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"readOnly": "#F1F2F3",
|
|
||||||
},
|
|
||||||
"borderColor": Object {
|
|
||||||
"disabled": "#E3E6E8",
|
|
||||||
"focus": "#454F54",
|
|
||||||
"hover": "#73848C",
|
|
||||||
"normal": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"disabled": "#ABB5BA",
|
|
||||||
"placeholder": Object {
|
|
||||||
"focus": Object {
|
|
||||||
"normal": "transparent",
|
|
||||||
"readOnly": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"normal": "#ABB5BA",
|
|
||||||
},
|
|
||||||
"readOnly": "#171A1C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"topicFormLabel": Object {
|
|
||||||
"color": "#73848C",
|
|
||||||
},
|
|
||||||
"topicMetaData": Object {
|
|
||||||
"backgroundColor": "#F1F2F3",
|
|
||||||
"color": Object {
|
|
||||||
"label": "#73848C",
|
|
||||||
"meta": "#ABB5BA",
|
|
||||||
"value": "#2F3639",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"topicsList": Object {
|
|
||||||
"backgroundColor": Object {
|
|
||||||
"active": "#E3E6E8",
|
|
||||||
"hover": "#F1F2F3",
|
|
||||||
},
|
|
||||||
"color": Object {
|
|
||||||
"active": "#171A1C",
|
|
||||||
"hover": "#73848C",
|
|
||||||
"normal": "#171A1C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"viewer": Object {
|
|
||||||
"wrapper": "#f9fafa",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SchemaVersion
|
|
||||||
version={
|
|
||||||
Object {
|
|
||||||
"compatibilityLevel": "BACKWARD",
|
|
||||||
"id": 1,
|
|
||||||
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
|
|
||||||
"schemaType": "JSON",
|
|
||||||
"subject": "test",
|
|
||||||
"version": "1",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ContextProvider>
|
|
||||||
`;
|
|
|
@ -1,8 +1,17 @@
|
||||||
import { SchemaSubject, SchemaType } from 'generated-sources';
|
import { SchemaSubject, SchemaType } from 'generated-sources';
|
||||||
|
import {
|
||||||
|
schemaVersion1,
|
||||||
|
schemaVersion2,
|
||||||
|
} from 'redux/reducers/schemas/__test__/fixtures';
|
||||||
|
|
||||||
|
export const versionPayload = [schemaVersion1, schemaVersion2];
|
||||||
|
export const versionEmptyPayload = [];
|
||||||
|
|
||||||
|
export const versions = [schemaVersion1, schemaVersion2];
|
||||||
|
|
||||||
export const jsonSchema: SchemaSubject = {
|
export const jsonSchema: SchemaSubject = {
|
||||||
subject: 'test',
|
subject: 'test',
|
||||||
version: '1',
|
version: '15',
|
||||||
id: 1,
|
id: 1,
|
||||||
schema:
|
schema:
|
||||||
'{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
'{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
||||||
|
@ -19,33 +28,3 @@ export const protoSchema: SchemaSubject = {
|
||||||
compatibilityLevel: 'BACKWARD',
|
compatibilityLevel: 'BACKWARD',
|
||||||
schemaType: SchemaType.PROTOBUF,
|
schemaType: SchemaType.PROTOBUF,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const versions: SchemaSubject[] = [
|
|
||||||
{
|
|
||||||
subject: 'test',
|
|
||||||
version: '1',
|
|
||||||
id: 1,
|
|
||||||
schema:
|
|
||||||
'{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
|
||||||
compatibilityLevel: 'BACKWARD',
|
|
||||||
schemaType: SchemaType.JSON,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
subject: 'test',
|
|
||||||
version: '2',
|
|
||||||
id: 2,
|
|
||||||
schema:
|
|
||||||
'{"type":"record","name":"MyRecord2","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
|
||||||
compatibilityLevel: 'BACKWARD',
|
|
||||||
schemaType: SchemaType.JSON,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
subject: 'test',
|
|
||||||
version: '3',
|
|
||||||
id: 3,
|
|
||||||
schema:
|
|
||||||
'syntax = "proto3";\npackage com.indeed;\n\nmessage MyRecord {\n int32 id = 1;\n string name = 2;\n}\n',
|
|
||||||
compatibilityLevel: 'BACKWARD',
|
|
||||||
schemaType: SchemaType.PROTOBUF,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
|
@ -16,11 +16,16 @@ import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
||||||
import {
|
import {
|
||||||
schemaAdded,
|
schemaAdded,
|
||||||
schemasApiClient,
|
schemasApiClient,
|
||||||
|
fetchLatestSchema,
|
||||||
|
getSchemaLatest,
|
||||||
|
SCHEMA_LATEST_FETCH_ACTION,
|
||||||
|
getAreSchemaLatestFulfilled,
|
||||||
schemaUpdated,
|
schemaUpdated,
|
||||||
selectSchemaById,
|
|
||||||
} from 'redux/reducers/schemas/schemasSlice';
|
} from 'redux/reducers/schemas/schemasSlice';
|
||||||
import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
|
import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
|
||||||
import { getResponse } from 'lib/errorHandling';
|
import { getResponse } from 'lib/errorHandling';
|
||||||
|
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||||
|
import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
|
||||||
|
|
||||||
import * as S from './Edit.styled';
|
import * as S from './Edit.styled';
|
||||||
|
|
||||||
|
@ -37,7 +42,15 @@ const Edit: React.FC = () => {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
} = methods;
|
} = methods;
|
||||||
|
|
||||||
const schema = useAppSelector((state) => selectSchemaById(state, subject));
|
React.useEffect(() => {
|
||||||
|
dispatch(fetchLatestSchema({ clusterName, subject }));
|
||||||
|
return () => {
|
||||||
|
dispatch(resetLoaderById(SCHEMA_LATEST_FETCH_ACTION));
|
||||||
|
};
|
||||||
|
}, [clusterName, subject]);
|
||||||
|
|
||||||
|
const schema = useAppSelector((state) => getSchemaLatest(state));
|
||||||
|
const isFetched = useAppSelector(getAreSchemaLatestFulfilled);
|
||||||
|
|
||||||
const formatedSchema = React.useMemo(() => {
|
const formatedSchema = React.useMemo(() => {
|
||||||
return schema?.schemaType === SchemaType.PROTOBUF
|
return schema?.schemaType === SchemaType.PROTOBUF
|
||||||
|
@ -84,8 +97,9 @@ const Edit: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!schema) return null;
|
if (!isFetched || !schema) {
|
||||||
|
return <PageLoader />;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<PageHeading text="Edit schema" />
|
<PageHeading text="Edit schema" />
|
||||||
|
|
|
@ -3,49 +3,77 @@ import Edit from 'components/Schemas/Edit/Edit';
|
||||||
import { render } from 'lib/testHelpers';
|
import { render } from 'lib/testHelpers';
|
||||||
import { clusterSchemaEditPath } from 'lib/paths';
|
import { clusterSchemaEditPath } from 'lib/paths';
|
||||||
import {
|
import {
|
||||||
schemasFulfilledState,
|
schemasInitialState,
|
||||||
schemaVersion,
|
schemaVersion,
|
||||||
} from 'redux/reducers/schemas/__test__/fixtures';
|
} from 'redux/reducers/schemas/__test__/fixtures';
|
||||||
import { Route } from 'react-router';
|
import { Route } from 'react-router';
|
||||||
import { screen } from '@testing-library/dom';
|
import { screen, waitFor } from '@testing-library/dom';
|
||||||
|
import ClusterContext, {
|
||||||
|
ContextProps,
|
||||||
|
initialValue as contextInitialValue,
|
||||||
|
} from 'components/contexts/ClusterContext';
|
||||||
|
import { RootState } from 'redux/interfaces';
|
||||||
|
import fetchMock from 'fetch-mock';
|
||||||
|
|
||||||
const clusterName = 'local';
|
const clusterName = 'testClusterName';
|
||||||
const { subject } = schemaVersion;
|
const schemasAPILatestUrl = `/api/clusters/${clusterName}/schemas/${schemaVersion.subject}/latest`;
|
||||||
|
|
||||||
describe('Edit Component', () => {
|
const renderComponent = (
|
||||||
describe('schema exists', () => {
|
initialState: RootState['schemas'] = schemasInitialState,
|
||||||
beforeEach(() => {
|
context: ContextProps = contextInitialValue
|
||||||
render(
|
) => {
|
||||||
<Route path={clusterSchemaEditPath(':clusterName', ':subject')}>
|
return render(
|
||||||
<Edit />
|
<Route path={clusterSchemaEditPath(':clusterName', ':subject')}>
|
||||||
</Route>,
|
<ClusterContext.Provider value={context}>
|
||||||
{
|
<Edit />
|
||||||
pathname: clusterSchemaEditPath(clusterName, subject),
|
</ClusterContext.Provider>
|
||||||
preloadedState: { schemas: schemasFulfilledState },
|
</Route>,
|
||||||
}
|
{
|
||||||
);
|
pathname: clusterSchemaEditPath(clusterName, schemaVersion.subject),
|
||||||
|
preloadedState: {
|
||||||
|
schemas: initialState,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Edit', () => {
|
||||||
|
afterEach(() => fetchMock.reset());
|
||||||
|
|
||||||
|
describe('fetch failed', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const schemasAPILatestMock = fetchMock.getOnce(schemasAPILatestUrl, 404);
|
||||||
|
renderComponent();
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(schemasAPILatestMock.called()).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders component', () => {
|
it('renders pageloader', () => {
|
||||||
expect(screen.getByText('Edit schema')).toBeInTheDocument();
|
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(schemaVersion.subject)).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Submit')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('schema does not exist', () => {
|
describe('fetch success', () => {
|
||||||
beforeEach(() => {
|
describe('has schema versions', () => {
|
||||||
render(
|
beforeEach(async () => {
|
||||||
<Route path={clusterSchemaEditPath(':clusterName', ':subject')}>
|
const schemasAPILatestMock = fetchMock.getOnce(
|
||||||
<Edit />
|
schemasAPILatestUrl,
|
||||||
</Route>,
|
schemaVersion
|
||||||
{
|
);
|
||||||
pathname: clusterSchemaEditPath(clusterName, 'fake'),
|
|
||||||
preloadedState: { schemas: schemasFulfilledState },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders component', () => {
|
renderComponent();
|
||||||
expect(screen.queryByText('Edit schema')).not.toBeInTheDocument();
|
await waitFor(() => {
|
||||||
|
expect(schemasAPILatestMock.called()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders component with schema info', () => {
|
||||||
|
expect(screen.getByText('Submit')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,6 +3,8 @@ import Select from 'components/common/Select/Select';
|
||||||
import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
|
import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
|
||||||
import { getResponse } from 'lib/errorHandling';
|
import { getResponse } from 'lib/errorHandling';
|
||||||
import { useAppDispatch } from 'lib/hooks/redux';
|
import { useAppDispatch } from 'lib/hooks/redux';
|
||||||
|
import usePagination from 'lib/hooks/usePagination';
|
||||||
|
import useSearch from 'lib/hooks/useSearch';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
|
import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
|
||||||
|
@ -16,6 +18,9 @@ import * as S from './GlobalSchemaSelector.styled';
|
||||||
const GlobalSchemaSelector: React.FC = () => {
|
const GlobalSchemaSelector: React.FC = () => {
|
||||||
const { clusterName } = useParams<{ clusterName: string }>();
|
const { clusterName } = useParams<{ clusterName: string }>();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const [searchText] = useSearch();
|
||||||
|
const { page, perPage } = usePagination();
|
||||||
|
|
||||||
const [currentCompatibilityLevel, setCurrentCompatibilityLevel] =
|
const [currentCompatibilityLevel, setCurrentCompatibilityLevel] =
|
||||||
React.useState<CompatibilityLevelCompatibilityEnum | undefined>();
|
React.useState<CompatibilityLevelCompatibilityEnum | undefined>();
|
||||||
const [nextCompatibilityLevel, setNextCompatibilityLevel] = React.useState<
|
const [nextCompatibilityLevel, setNextCompatibilityLevel] = React.useState<
|
||||||
|
@ -61,7 +66,9 @@ const GlobalSchemaSelector: React.FC = () => {
|
||||||
setCurrentCompatibilityLevel(nextCompatibilityLevel);
|
setCurrentCompatibilityLevel(nextCompatibilityLevel);
|
||||||
setNextCompatibilityLevel(undefined);
|
setNextCompatibilityLevel(undefined);
|
||||||
setIsConfirmationVisible(false);
|
setIsConfirmationVisible(false);
|
||||||
dispatch(fetchSchemas(clusterName));
|
dispatch(
|
||||||
|
fetchSchemas({ clusterName, page, perPage, search: searchText })
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const err = await getResponse(e as Response);
|
const err = await getResponse(e as Response);
|
||||||
dispatch(serverErrorAlertAdded(err));
|
dispatch(serverErrorAlertAdded(err));
|
||||||
|
|
|
@ -6,16 +6,42 @@ import * as C from 'components/common/table/Table/Table.styled';
|
||||||
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
||||||
import { Button } from 'components/common/Button/Button';
|
import { Button } from 'components/common/Button/Button';
|
||||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||||
import { useAppSelector } from 'lib/hooks/redux';
|
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
||||||
import { selectAllSchemas } from 'redux/reducers/schemas/schemasSlice';
|
import {
|
||||||
|
selectAllSchemas,
|
||||||
|
fetchSchemas,
|
||||||
|
getAreSchemasFulfilled,
|
||||||
|
SCHEMAS_FETCH_ACTION,
|
||||||
|
} from 'redux/reducers/schemas/schemasSlice';
|
||||||
|
import usePagination from 'lib/hooks/usePagination';
|
||||||
|
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||||
|
import Pagination from 'components/common/Pagination/Pagination';
|
||||||
|
import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
|
||||||
|
import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
|
||||||
|
import Search from 'components/common/Search/Search';
|
||||||
|
import useSearch from 'lib/hooks/useSearch';
|
||||||
|
|
||||||
import ListItem from './ListItem';
|
import ListItem from './ListItem';
|
||||||
import GlobalSchemaSelector from './GlobalSchemaSelector/GlobalSchemaSelector';
|
import GlobalSchemaSelector from './GlobalSchemaSelector/GlobalSchemaSelector';
|
||||||
|
|
||||||
const List: React.FC = () => {
|
const List: React.FC = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const { isReadOnly } = React.useContext(ClusterContext);
|
const { isReadOnly } = React.useContext(ClusterContext);
|
||||||
const { clusterName } = useParams<{ clusterName: string }>();
|
const { clusterName } = useParams<{ clusterName: string }>();
|
||||||
|
|
||||||
const schemas = useAppSelector(selectAllSchemas);
|
const schemas = useAppSelector(selectAllSchemas);
|
||||||
|
const isFetched = useAppSelector(getAreSchemasFulfilled);
|
||||||
|
const totalPages = useAppSelector((state) => state.schemas.totalPages);
|
||||||
|
|
||||||
|
const [searchText, handleSearchText] = useSearch();
|
||||||
|
const { page, perPage } = usePagination();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
dispatch(fetchSchemas({ clusterName, page, perPage, search: searchText }));
|
||||||
|
return () => {
|
||||||
|
dispatch(resetLoaderById(SCHEMAS_FETCH_ACTION));
|
||||||
|
};
|
||||||
|
}, [clusterName, page, perPage, searchText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -34,28 +60,42 @@ const List: React.FC = () => {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</PageHeading>
|
</PageHeading>
|
||||||
<C.Table isFullwidth>
|
<ControlPanelWrapper hasInput>
|
||||||
<thead>
|
<Search
|
||||||
<tr>
|
placeholder="Search by Schema Name"
|
||||||
<TableHeaderCell title="Schema Name" />
|
value={searchText}
|
||||||
<TableHeaderCell title="Version" />
|
handleSearch={handleSearchText}
|
||||||
<TableHeaderCell title="Compatibility" />
|
/>
|
||||||
</tr>
|
</ControlPanelWrapper>
|
||||||
</thead>
|
{isFetched ? (
|
||||||
<tbody>
|
<>
|
||||||
{schemas.length === 0 && (
|
<C.Table isFullwidth>
|
||||||
<tr>
|
<thead>
|
||||||
<td colSpan={10}>No schemas found</td>
|
<tr>
|
||||||
</tr>
|
<TableHeaderCell title="Schema Name" />
|
||||||
)}
|
<TableHeaderCell title="Version" />
|
||||||
{schemas.map((subject) => (
|
<TableHeaderCell title="Compatibility" />
|
||||||
<ListItem
|
</tr>
|
||||||
key={[subject.id, subject.subject].join('-')}
|
</thead>
|
||||||
subject={subject}
|
<tbody>
|
||||||
/>
|
{schemas.length === 0 && (
|
||||||
))}
|
<tr>
|
||||||
</tbody>
|
<td colSpan={10}>No schemas found</td>
|
||||||
</C.Table>
|
</tr>
|
||||||
|
)}
|
||||||
|
{schemas.map((subject) => (
|
||||||
|
<ListItem
|
||||||
|
key={[subject.id, subject.subject].join('-')}
|
||||||
|
subject={subject}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</C.Table>
|
||||||
|
<Pagination totalPages={totalPages} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<PageLoader />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,60 +3,117 @@ import List from 'components/Schemas/List/List';
|
||||||
import { render } from 'lib/testHelpers';
|
import { render } from 'lib/testHelpers';
|
||||||
import { Route } from 'react-router';
|
import { Route } from 'react-router';
|
||||||
import { clusterSchemasPath } from 'lib/paths';
|
import { clusterSchemasPath } from 'lib/paths';
|
||||||
import { screen } from '@testing-library/dom';
|
import { screen, waitFor } from '@testing-library/dom';
|
||||||
import {
|
import {
|
||||||
schemasFulfilledState,
|
schemasFulfilledState,
|
||||||
schemasInitialState,
|
schemasInitialState,
|
||||||
|
schemaVersion1,
|
||||||
|
schemaVersion2,
|
||||||
} from 'redux/reducers/schemas/__test__/fixtures';
|
} from 'redux/reducers/schemas/__test__/fixtures';
|
||||||
import ClusterContext, {
|
import ClusterContext, {
|
||||||
ContextProps,
|
ContextProps,
|
||||||
initialValue as contextInitialValue,
|
initialValue as contextInitialValue,
|
||||||
} from 'components/contexts/ClusterContext';
|
} from 'components/contexts/ClusterContext';
|
||||||
import { RootState } from 'redux/interfaces';
|
import { RootState } from 'redux/interfaces';
|
||||||
|
import fetchMock from 'fetch-mock';
|
||||||
|
|
||||||
|
import { schemasPayload, schemasEmptyPayload } from './fixtures';
|
||||||
|
|
||||||
const clusterName = 'testClusterName';
|
const clusterName = 'testClusterName';
|
||||||
|
const schemasAPIUrl = `/api/clusters/${clusterName}/schemas`;
|
||||||
|
const schemasAPICompabilityUrl = `${schemasAPIUrl}/compatibility`;
|
||||||
const renderComponent = (
|
const renderComponent = (
|
||||||
initialState: RootState['schemas'] = schemasInitialState,
|
initialState: RootState['schemas'] = schemasInitialState,
|
||||||
context: ContextProps = contextInitialValue
|
context: ContextProps = contextInitialValue
|
||||||
) =>
|
) =>
|
||||||
render(
|
render(
|
||||||
<ClusterContext.Provider value={context}>
|
<Route path={clusterSchemasPath(':clusterName')}>
|
||||||
<Route path={clusterSchemasPath(':clusterName')}>
|
<ClusterContext.Provider value={context}>
|
||||||
<List />
|
<List />
|
||||||
</Route>
|
</ClusterContext.Provider>
|
||||||
</ClusterContext.Provider>,
|
</Route>,
|
||||||
{
|
{
|
||||||
pathname: clusterSchemasPath(clusterName),
|
pathname: clusterSchemasPath(clusterName),
|
||||||
preloadedState: {
|
preloadedState: {
|
||||||
loader: {
|
|
||||||
'schemas/fetch': 'fulfilled',
|
|
||||||
},
|
|
||||||
schemas: initialState,
|
schemas: initialState,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('List', () => {
|
describe('List', () => {
|
||||||
it('renders list', () => {
|
afterEach(() => {
|
||||||
renderComponent(schemasFulfilledState);
|
fetchMock.reset();
|
||||||
expect(screen.getByText('MySchemaSubject')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('schema7_1')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders empty table', () => {
|
describe('fetch error', () => {
|
||||||
renderComponent();
|
it('shows progressbar', async () => {
|
||||||
expect(screen.getByText('No schemas found')).toBeInTheDocument();
|
const fetchSchemasMock = fetchMock.getOnce(schemasAPIUrl, 404);
|
||||||
|
const fetchCompabilityMock = fetchMock.getOnce(
|
||||||
|
schemasAPICompabilityUrl,
|
||||||
|
404
|
||||||
|
);
|
||||||
|
renderComponent();
|
||||||
|
await waitFor(() => expect(fetchSchemasMock.called()).toBeTruthy());
|
||||||
|
await waitFor(() => expect(fetchCompabilityMock.called()).toBeTruthy());
|
||||||
|
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with readonly cluster', () => {
|
describe('fetch success', () => {
|
||||||
it('does not render Create Schema button', () => {
|
describe('responded without schemas', () => {
|
||||||
renderComponent(schemasFulfilledState, {
|
beforeEach(async () => {
|
||||||
...contextInitialValue,
|
const fetchSchemasMock = fetchMock.getOnce(
|
||||||
isReadOnly: true,
|
schemasAPIUrl,
|
||||||
|
schemasEmptyPayload
|
||||||
|
);
|
||||||
|
const fetchCompabilityMock = fetchMock.getOnce(
|
||||||
|
schemasAPICompabilityUrl,
|
||||||
|
200
|
||||||
|
);
|
||||||
|
renderComponent();
|
||||||
|
await waitFor(() => expect(fetchSchemasMock.called()).toBeTruthy());
|
||||||
|
await waitFor(() => expect(fetchCompabilityMock.called()).toBeTruthy());
|
||||||
});
|
});
|
||||||
|
it('renders empty table', () => {
|
||||||
|
expect(screen.getByText('No schemas found')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('responded with schemas', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const fetchSchemasMock = fetchMock.getOnce(
|
||||||
|
schemasAPIUrl,
|
||||||
|
schemasPayload
|
||||||
|
);
|
||||||
|
const fetchCompabilityMock = fetchMock.getOnce(
|
||||||
|
schemasAPICompabilityUrl,
|
||||||
|
200
|
||||||
|
);
|
||||||
|
renderComponent(schemasFulfilledState);
|
||||||
|
await waitFor(() => expect(fetchSchemasMock.called()).toBeTruthy());
|
||||||
|
await waitFor(() => expect(fetchCompabilityMock.called()).toBeTruthy());
|
||||||
|
});
|
||||||
|
it('renders list', () => {
|
||||||
|
expect(screen.getByText(schemaVersion1.subject)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(schemaVersion2.subject)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
expect(screen.queryByText('Create Schema')).not.toBeInTheDocument();
|
describe('responded with readonly cluster schemas', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const fetchSchemasMock = fetchMock.getOnce(
|
||||||
|
schemasAPIUrl,
|
||||||
|
schemasPayload
|
||||||
|
);
|
||||||
|
|
||||||
|
renderComponent(schemasFulfilledState, {
|
||||||
|
...contextInitialValue,
|
||||||
|
isReadOnly: true,
|
||||||
|
});
|
||||||
|
await waitFor(() => expect(fetchSchemasMock.called()).toBeTruthy());
|
||||||
|
});
|
||||||
|
it('does not render Create Schema button', () => {
|
||||||
|
expect(screen.queryByText('Create Schema')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,31 +1,16 @@
|
||||||
import { SchemaSubject, SchemaType } from 'generated-sources';
|
import {
|
||||||
|
schemaVersion1,
|
||||||
|
schemaVersion2,
|
||||||
|
} from 'redux/reducers/schemas/__test__/fixtures';
|
||||||
|
|
||||||
export const schemas: SchemaSubject[] = [
|
export const schemas = [schemaVersion1, schemaVersion2];
|
||||||
{
|
|
||||||
subject: 'test',
|
export const schemasPayload = {
|
||||||
version: '1',
|
pageCount: 1,
|
||||||
id: 1,
|
schemas,
|
||||||
schema:
|
};
|
||||||
'{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
|
||||||
compatibilityLevel: 'BACKWARD',
|
export const schemasEmptyPayload = {
|
||||||
schemaType: SchemaType.JSON,
|
pageCount: 1,
|
||||||
},
|
schemas: [],
|
||||||
{
|
};
|
||||||
subject: 'test2',
|
|
||||||
version: '1',
|
|
||||||
id: 2,
|
|
||||||
schema:
|
|
||||||
'{"type":"record","name":"MyRecord2","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
|
||||||
compatibilityLevel: 'BACKWARD',
|
|
||||||
schemaType: SchemaType.JSON,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
subject: 'test3',
|
|
||||||
version: '1',
|
|
||||||
id: 12,
|
|
||||||
schema:
|
|
||||||
'{"type":"record","name":"MyRecord3","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
|
||||||
compatibilityLevel: 'BACKWARD',
|
|
||||||
schemaType: SchemaType.JSON,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
|
@ -1,17 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Switch, useParams } from 'react-router-dom';
|
import { Switch } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
clusterSchemaNewPath,
|
clusterSchemaNewPath,
|
||||||
clusterSchemaPath,
|
clusterSchemaPath,
|
||||||
clusterSchemaEditPath,
|
clusterSchemaEditPath,
|
||||||
clusterSchemasPath,
|
clusterSchemasPath,
|
||||||
} from 'lib/paths';
|
} from 'lib/paths';
|
||||||
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
|
||||||
import {
|
|
||||||
fetchSchemas,
|
|
||||||
getAreSchemasFulfilled,
|
|
||||||
} from 'redux/reducers/schemas/schemasSlice';
|
|
||||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
|
||||||
import List from 'components/Schemas/List/List';
|
import List from 'components/Schemas/List/List';
|
||||||
import Details from 'components/Schemas/Details/Details';
|
import Details from 'components/Schemas/Details/Details';
|
||||||
import New from 'components/Schemas/New/New';
|
import New from 'components/Schemas/New/New';
|
||||||
|
@ -19,18 +13,6 @@ import Edit from 'components/Schemas/Edit/Edit';
|
||||||
import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
|
import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
|
||||||
|
|
||||||
const Schemas: React.FC = () => {
|
const Schemas: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { clusterName } = useParams<{ clusterName: string }>();
|
|
||||||
const isFetched = useAppSelector(getAreSchemasFulfilled);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
dispatch(fetchSchemas(clusterName));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!isFetched) {
|
|
||||||
return <PageLoader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<BreadcrumbRoute
|
<BreadcrumbRoute
|
||||||
|
|
|
@ -3,6 +3,7 @@ import usePagination from 'lib/hooks/usePagination';
|
||||||
import { range } from 'lodash';
|
import { range } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PageControl from 'components/common/Pagination/PageControl';
|
import PageControl from 'components/common/Pagination/PageControl';
|
||||||
|
import useSearch from 'lib/hooks/useSearch';
|
||||||
|
|
||||||
import * as S from './Pagination.styled';
|
import * as S from './Pagination.styled';
|
||||||
|
|
||||||
|
@ -14,12 +15,17 @@ const NEIGHBOURS = 2;
|
||||||
|
|
||||||
const Pagination: React.FC<PaginationProps> = ({ totalPages }) => {
|
const Pagination: React.FC<PaginationProps> = ({ totalPages }) => {
|
||||||
const { page, perPage, pathname } = usePagination();
|
const { page, perPage, pathname } = usePagination();
|
||||||
|
const [searchText] = useSearch();
|
||||||
|
|
||||||
const currentPage = page || 1;
|
const currentPage = page || 1;
|
||||||
const currentPerPage = perPage || PER_PAGE;
|
const currentPerPage = perPage || PER_PAGE;
|
||||||
|
|
||||||
|
const searchParam = searchText ? `&q=${searchText}` : '';
|
||||||
const getPath = (newPage: number) =>
|
const getPath = (newPage: number) =>
|
||||||
`${pathname}?page=${Math.max(newPage, 1)}&perPage=${currentPerPage}`;
|
`${pathname}?page=${Math.max(
|
||||||
|
newPage,
|
||||||
|
1
|
||||||
|
)}&perPage=${currentPerPage}${searchParam}`;
|
||||||
|
|
||||||
const pages = React.useMemo(() => {
|
const pages = React.useMemo(() => {
|
||||||
// Total visible numbers: neighbours, current, first & last
|
// Total visible numbers: neighbours, current, first & last
|
||||||
|
|
|
@ -17,6 +17,7 @@ const Search: React.FC<SearchProps> = ({
|
||||||
(e) => handleSearch(e.target.value),
|
(e) => handleSearch(e.target.value),
|
||||||
300
|
300
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
48
kafka-ui-react-app/src/lib/hooks/useSearch.ts
Normal file
48
kafka-ui-react-app/src/lib/hooks/useSearch.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { useHistory, useLocation } from 'react-router';
|
||||||
|
|
||||||
|
const SEARCH_QUERY_ARG = 'q';
|
||||||
|
|
||||||
|
// meant for use with <Search> component
|
||||||
|
// returns value of Q search param (?q='something') and callback to change it
|
||||||
|
const useSearch = (initValue = ''): [string, (value: string) => void] => {
|
||||||
|
const history = useHistory();
|
||||||
|
const { search, pathname } = useLocation();
|
||||||
|
const queryParams = useMemo(() => new URLSearchParams(search), [search]);
|
||||||
|
const q = useMemo(
|
||||||
|
() => queryParams.get(SEARCH_QUERY_ARG)?.trim(),
|
||||||
|
[queryParams]
|
||||||
|
);
|
||||||
|
const page = useMemo(() => queryParams.get('page')?.trim(), [queryParams]);
|
||||||
|
|
||||||
|
// set intial value
|
||||||
|
useEffect(() => {
|
||||||
|
if (initValue.trim() !== '' && !q) {
|
||||||
|
queryParams.set(SEARCH_QUERY_ARG, initValue.trim());
|
||||||
|
history.push({ pathname, search: queryParams.toString() });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
const trimmedValue = value.trim();
|
||||||
|
if (trimmedValue !== q) {
|
||||||
|
if (trimmedValue) {
|
||||||
|
queryParams.set(SEARCH_QUERY_ARG, trimmedValue);
|
||||||
|
} else {
|
||||||
|
queryParams.delete(SEARCH_QUERY_ARG);
|
||||||
|
}
|
||||||
|
// If we were on page 3 we can't determine if new search results have 3 pages - so we always reset page
|
||||||
|
if (page) {
|
||||||
|
queryParams.delete('page');
|
||||||
|
}
|
||||||
|
history.replace({ pathname, search: queryParams.toString() });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[history, pathname, queryParams, q]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [q || initValue.trim() || '', handleChange];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSearch;
|
|
@ -1,6 +1,18 @@
|
||||||
import { SchemaType } from 'generated-sources';
|
import { SchemaType, SchemaSubject } from 'generated-sources';
|
||||||
|
import { RootState } from 'redux/interfaces';
|
||||||
|
|
||||||
export const schemaVersion = {
|
export const schemasInitialState: RootState['schemas'] = {
|
||||||
|
totalPages: 0,
|
||||||
|
ids: [],
|
||||||
|
entities: {},
|
||||||
|
versions: {
|
||||||
|
latest: null,
|
||||||
|
ids: [],
|
||||||
|
entities: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const schemaVersion1: SchemaSubject = {
|
||||||
subject: 'schema7_1',
|
subject: 'schema7_1',
|
||||||
version: '1',
|
version: '1',
|
||||||
id: 2,
|
id: 2,
|
||||||
|
@ -9,31 +21,41 @@ export const schemaVersion = {
|
||||||
compatibilityLevel: 'FULL',
|
compatibilityLevel: 'FULL',
|
||||||
schemaType: SchemaType.JSON,
|
schemaType: SchemaType.JSON,
|
||||||
};
|
};
|
||||||
|
export const schemaVersion2: SchemaSubject = {
|
||||||
|
subject: 'MySchemaSubject',
|
||||||
|
version: '2',
|
||||||
|
id: 28,
|
||||||
|
schema: '12',
|
||||||
|
compatibilityLevel: 'FORWARD_TRANSITIVE',
|
||||||
|
schemaType: SchemaType.JSON,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { schemaVersion1 as schemaVersion };
|
||||||
|
|
||||||
export const schemasFulfilledState = {
|
export const schemasFulfilledState = {
|
||||||
ids: ['MySchemaSubject', 'schema7_1'],
|
totalPages: 1,
|
||||||
|
ids: [schemaVersion2.subject, schemaVersion1.subject],
|
||||||
entities: {
|
entities: {
|
||||||
MySchemaSubject: {
|
[schemaVersion2.subject]: schemaVersion2,
|
||||||
subject: 'MySchemaSubject',
|
[schemaVersion1.subject]: schemaVersion1,
|
||||||
version: '1',
|
|
||||||
id: 28,
|
|
||||||
schema: '12',
|
|
||||||
compatibilityLevel: 'FORWARD_TRANSITIVE',
|
|
||||||
schemaType: SchemaType.JSON,
|
|
||||||
},
|
|
||||||
schema7_1: schemaVersion,
|
|
||||||
},
|
},
|
||||||
versions: {
|
versions: {
|
||||||
|
latest: null,
|
||||||
ids: [],
|
ids: [],
|
||||||
entities: {},
|
entities: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const schemasInitialState = {
|
export const versionFulfilledState = {
|
||||||
|
totalPages: 1,
|
||||||
ids: [],
|
ids: [],
|
||||||
entities: {},
|
entities: {},
|
||||||
versions: {
|
versions: {
|
||||||
ids: [],
|
latest: schemaVersion2,
|
||||||
entities: {},
|
ids: [schemaVersion1.id, schemaVersion2.id],
|
||||||
|
entities: {
|
||||||
|
[schemaVersion2.id]: schemaVersion2,
|
||||||
|
[schemaVersion1.id]: schemaVersion1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,14 @@ import {
|
||||||
createSelector,
|
createSelector,
|
||||||
createSlice,
|
createSlice,
|
||||||
} from '@reduxjs/toolkit';
|
} from '@reduxjs/toolkit';
|
||||||
import { Configuration, SchemasApi, SchemaSubject } from 'generated-sources';
|
import {
|
||||||
|
Configuration,
|
||||||
|
SchemasApi,
|
||||||
|
SchemaSubject,
|
||||||
|
SchemaSubjectsResponse,
|
||||||
|
GetSchemasRequest,
|
||||||
|
GetLatestSchemaRequest,
|
||||||
|
} from 'generated-sources';
|
||||||
import { BASE_PARAMS } from 'lib/constants';
|
import { BASE_PARAMS } from 'lib/constants';
|
||||||
import { getResponse } from 'lib/errorHandling';
|
import { getResponse } from 'lib/errorHandling';
|
||||||
import { ClusterName, RootState } from 'redux/interfaces';
|
import { ClusterName, RootState } from 'redux/interfaces';
|
||||||
|
@ -13,21 +20,44 @@ import { createFetchingSelector } from 'redux/reducers/loader/selectors';
|
||||||
const apiClientConf = new Configuration(BASE_PARAMS);
|
const apiClientConf = new Configuration(BASE_PARAMS);
|
||||||
export const schemasApiClient = new SchemasApi(apiClientConf);
|
export const schemasApiClient = new SchemasApi(apiClientConf);
|
||||||
|
|
||||||
export const fetchSchemas = createAsyncThunk<SchemaSubject[], ClusterName>(
|
export const SCHEMA_LATEST_FETCH_ACTION = 'schemas/latest/fetch';
|
||||||
'schemas/fetch',
|
export const fetchLatestSchema = createAsyncThunk<
|
||||||
async (clusterName: ClusterName, { rejectWithValue }) => {
|
SchemaSubject,
|
||||||
|
GetLatestSchemaRequest
|
||||||
|
>(SCHEMA_LATEST_FETCH_ACTION, async (schemaParams, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
return await schemasApiClient.getLatestSchema(schemaParams);
|
||||||
|
} catch (error) {
|
||||||
|
return rejectWithValue(await getResponse(error as Response));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SCHEMAS_FETCH_ACTION = 'schemas/fetch';
|
||||||
|
export const fetchSchemas = createAsyncThunk<
|
||||||
|
SchemaSubjectsResponse,
|
||||||
|
GetSchemasRequest
|
||||||
|
>(
|
||||||
|
SCHEMAS_FETCH_ACTION,
|
||||||
|
async ({ clusterName, page, perPage, search }, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
return await schemasApiClient.getSchemas({ clusterName });
|
return await schemasApiClient.getSchemas({
|
||||||
|
clusterName,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
search: search || undefined,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return rejectWithValue(await getResponse(error as Response));
|
return rejectWithValue(await getResponse(error as Response));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const SCHEMAS_VERSIONS_FETCH_ACTION = 'schemas/versions/fetch';
|
||||||
export const fetchSchemaVersions = createAsyncThunk<
|
export const fetchSchemaVersions = createAsyncThunk<
|
||||||
SchemaSubject[],
|
SchemaSubject[],
|
||||||
{ clusterName: ClusterName; subject: SchemaSubject['subject'] }
|
{ clusterName: ClusterName; subject: SchemaSubject['subject'] }
|
||||||
>(
|
>(
|
||||||
'schemas/versions/fetch',
|
SCHEMAS_VERSIONS_FETCH_ACTION,
|
||||||
async ({ clusterName, subject }, { rejectWithValue }) => {
|
async ({ clusterName, subject }, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
return await schemasApiClient.getAllVersionsBySubject({
|
return await schemasApiClient.getAllVersionsBySubject({
|
||||||
|
@ -48,18 +78,31 @@ const schemasAdapter = createEntityAdapter<SchemaSubject>({
|
||||||
selectId: ({ subject }) => subject,
|
selectId: ({ subject }) => subject,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const SCHEMAS_PAGE_COUNT = 1;
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
totalPages: SCHEMAS_PAGE_COUNT,
|
||||||
|
...schemasAdapter.getInitialState(),
|
||||||
|
versions: {
|
||||||
|
...schemaVersionsAdapter.getInitialState(),
|
||||||
|
latest: <SchemaSubject | null>null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const schemasSlice = createSlice({
|
const schemasSlice = createSlice({
|
||||||
name: 'schemas',
|
name: 'schemas',
|
||||||
initialState: schemasAdapter.getInitialState({
|
initialState,
|
||||||
versions: schemaVersionsAdapter.getInitialState(),
|
|
||||||
}),
|
|
||||||
reducers: {
|
reducers: {
|
||||||
schemaAdded: schemasAdapter.addOne,
|
schemaAdded: schemasAdapter.addOne,
|
||||||
schemaUpdated: schemasAdapter.upsertOne,
|
schemaUpdated: schemasAdapter.upsertOne,
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(fetchSchemas.fulfilled, (state, { payload }) => {
|
builder.addCase(fetchSchemas.fulfilled, (state, { payload }) => {
|
||||||
schemasAdapter.setAll(state, payload);
|
state.totalPages = payload.pageCount || SCHEMAS_PAGE_COUNT;
|
||||||
|
schemasAdapter.setAll(state, payload.schemas || []);
|
||||||
|
});
|
||||||
|
builder.addCase(fetchLatestSchema.fulfilled, (state, { payload }) => {
|
||||||
|
state.versions.latest = payload;
|
||||||
});
|
});
|
||||||
builder.addCase(fetchSchemaVersions.fulfilled, (state, { payload }) => {
|
builder.addCase(fetchSchemaVersions.fulfilled, (state, { payload }) => {
|
||||||
schemaVersionsAdapter.setAll(state.versions, payload);
|
schemaVersionsAdapter.setAll(state.versions, payload);
|
||||||
|
@ -70,19 +113,32 @@ const schemasSlice = createSlice({
|
||||||
export const { selectAll: selectAllSchemas, selectById: selectSchemaById } =
|
export const { selectAll: selectAllSchemas, selectById: selectSchemaById } =
|
||||||
schemasAdapter.getSelectors<RootState>((state) => state.schemas);
|
schemasAdapter.getSelectors<RootState>((state) => state.schemas);
|
||||||
|
|
||||||
export const { selectAll: selectAllSchemaVersions } =
|
export const {
|
||||||
schemaVersionsAdapter.getSelectors<RootState>(
|
selectAll: selectAllSchemaVersions,
|
||||||
(state) => state.schemas.versions
|
selectById: selectVersionSchemaByID,
|
||||||
);
|
} = schemaVersionsAdapter.getSelectors<RootState>(
|
||||||
|
(state) => state.schemas.versions
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSchemaVersions = (state: RootState) => state.schemas.versions;
|
||||||
|
export const getSchemaLatest = createSelector(
|
||||||
|
getSchemaVersions,
|
||||||
|
(state) => state.latest
|
||||||
|
);
|
||||||
|
|
||||||
export const { schemaAdded, schemaUpdated } = schemasSlice.actions;
|
export const { schemaAdded, schemaUpdated } = schemasSlice.actions;
|
||||||
|
|
||||||
export const getAreSchemasFulfilled = createSelector(
|
export const getAreSchemasFulfilled = createSelector(
|
||||||
createFetchingSelector('schemas/fetch'),
|
createFetchingSelector(SCHEMAS_FETCH_ACTION),
|
||||||
|
(status) => status === 'fulfilled'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getAreSchemaLatestFulfilled = createSelector(
|
||||||
|
createFetchingSelector(SCHEMA_LATEST_FETCH_ACTION),
|
||||||
(status) => status === 'fulfilled'
|
(status) => status === 'fulfilled'
|
||||||
);
|
);
|
||||||
export const getAreSchemaVersionsFulfilled = createSelector(
|
export const getAreSchemaVersionsFulfilled = createSelector(
|
||||||
createFetchingSelector('schemas/versions/fetch'),
|
createFetchingSelector(SCHEMAS_VERSIONS_FETCH_ACTION),
|
||||||
(status) => status === 'fulfilled'
|
(status) => status === 'fulfilled'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue