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.NewSchemaSubjectDTO;
|
||||
import com.provectus.kafka.ui.model.SchemaSubjectDTO;
|
||||
import com.provectus.kafka.ui.model.SchemaSubjectsResponseDTO;
|
||||
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 lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
@ -22,6 +29,8 @@ import reactor.core.publisher.Mono;
|
|||
@Slf4j
|
||||
public class SchemasController extends AbstractController implements SchemasApi {
|
||||
|
||||
private static final Integer DEFAULT_PAGE_SIZE = 25;
|
||||
|
||||
private final SchemaRegistryService schemaRegistryService;
|
||||
|
||||
@Override
|
||||
|
@ -102,12 +111,29 @@ public class SchemasController extends AbstractController implements SchemasApi
|
|||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResponseEntity<Flux<SchemaSubjectDTO>>> getSchemas(String clusterName,
|
||||
ServerWebExchange exchange) {
|
||||
Flux<SchemaSubjectDTO> subjects = schemaRegistryService.getAllLatestVersionSchemas(
|
||||
getCluster(clusterName)
|
||||
);
|
||||
return Mono.just(ResponseEntity.ok(subjects));
|
||||
public Mono<ResponseEntity<SchemaSubjectsResponseDTO>> getSchemas(String clusterName,
|
||||
@Valid Integer pageNum,
|
||||
@Valid Integer perPage,
|
||||
@Valid String search,
|
||||
ServerWebExchange serverWebExchange) {
|
||||
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
|
||||
|
|
|
@ -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.SubjectIdResponse;
|
||||
import java.util.Formatter;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
@ -43,6 +45,7 @@ import reactor.core.publisher.Mono;
|
|||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
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 = "No such schema %s";
|
||||
|
||||
|
@ -57,11 +60,11 @@ public class SchemaRegistryService {
|
|||
private final ClusterMapper mapper;
|
||||
private final WebClient webClient;
|
||||
|
||||
public Flux<SchemaSubjectDTO> getAllLatestVersionSchemas(KafkaCluster cluster) {
|
||||
var allSubjectNames = getAllSubjectNames(cluster);
|
||||
return allSubjectNames
|
||||
.flatMapMany(Flux::fromArray)
|
||||
.flatMap(subject -> getLatestSchemaVersionBySubject(cluster, subject));
|
||||
public Mono<List<SchemaSubjectDTO>> getAllLatestVersionSchemas(KafkaCluster cluster,
|
||||
List<String> subjects) {
|
||||
return Flux.fromIterable(subjects)
|
||||
.concatMap(subject -> getLatestSchemaVersionBySubject(cluster, subject))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
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.NewSchemaSubjectDTO;
|
||||
import com.provectus.kafka.ui.model.SchemaSubjectDTO;
|
||||
import com.provectus.kafka.ui.model.SchemaSubjectsResponseDTO;
|
||||
import com.provectus.kafka.ui.model.SchemaTypeDTO;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
@ -145,14 +146,14 @@ class SchemaRegistryServiceTests extends AbstractBaseTest {
|
|||
.uri("/api/clusters/{clusterName}/schemas", LOCAL)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBodyList(SchemaSubjectDTO.class)
|
||||
.expectBody(SchemaSubjectsResponseDTO.class)
|
||||
.consumeWith(result -> {
|
||||
List<SchemaSubjectDTO> responseBody = result.getResponseBody();
|
||||
SchemaSubjectsResponseDTO responseBody = result.getResponseBody();
|
||||
log.info("Response of test schemas: {}", 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()))
|
||||
.findFirst()
|
||||
.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
|
||||
schema:
|
||||
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:
|
||||
200:
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SchemaSubject'
|
||||
$ref: '#/components/schemas/SchemaSubjectsResponse'
|
||||
|
||||
/api/clusters/{clusterName}/schemas/{subject}:
|
||||
delete:
|
||||
|
@ -2248,6 +2261,16 @@ components:
|
|||
required:
|
||||
- isCompatible
|
||||
|
||||
SchemaSubjectsResponse:
|
||||
type: object
|
||||
properties:
|
||||
pageCount:
|
||||
type: integer
|
||||
schemas:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SchemaSubject'
|
||||
|
||||
Connect:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
@ -13,15 +13,19 @@ import { Table } from 'components/common/table/Table/Table.styled';
|
|||
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
||||
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
||||
import {
|
||||
fetchLatestSchema,
|
||||
fetchSchemaVersions,
|
||||
getAreSchemasFulfilled,
|
||||
getAreSchemaLatestFulfilled,
|
||||
getAreSchemaVersionsFulfilled,
|
||||
schemasApiClient,
|
||||
SCHEMAS_VERSIONS_FETCH_ACTION,
|
||||
SCHEMA_LATEST_FETCH_ACTION,
|
||||
selectAllSchemaVersions,
|
||||
selectSchemaById,
|
||||
getSchemaLatest,
|
||||
} from 'redux/reducers/schemas/schemasSlice';
|
||||
import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
|
||||
import { getResponse } from 'lib/errorHandling';
|
||||
import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
|
||||
|
||||
import LatestVersionItem from './LatestVersion/LatestVersionItem';
|
||||
import SchemaVersion from './SchemaVersion/SchemaVersion';
|
||||
|
@ -39,15 +43,23 @@ const Details: React.FC = () => {
|
|||
] = React.useState(false);
|
||||
|
||||
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 schema = useAppSelector((state) => selectSchemaById(state, subject));
|
||||
const versions = useAppSelector((state) =>
|
||||
selectAllSchemaVersions(state).filter((v) => v.subject === subject)
|
||||
);
|
||||
|
||||
const onDelete = React.useCallback(async () => {
|
||||
try {
|
||||
|
@ -62,7 +74,7 @@ const Details: React.FC = () => {
|
|||
}
|
||||
}, [clusterName, subject]);
|
||||
|
||||
if (!areSchemasFetched || !schema) {
|
||||
if (!isFetched || !schema) {
|
||||
return <PageLoader />;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,111 +5,119 @@ import { Route } from 'react-router';
|
|||
import { clusterSchemaPath } from 'lib/paths';
|
||||
import { screen, waitFor } from '@testing-library/dom';
|
||||
import {
|
||||
schemasFulfilledState,
|
||||
schemasInitialState,
|
||||
schemaVersion,
|
||||
} from 'redux/reducers/schemas/__test__/fixtures';
|
||||
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 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('for an initial state', () => {
|
||||
it('renders pageloader', () => {
|
||||
render(
|
||||
<Route path={clusterSchemaPath(':clusterName', ':subject')}>
|
||||
<Details />
|
||||
</Route>,
|
||||
{
|
||||
pathname: clusterSchemaPath(clusterName, subject),
|
||||
preloadedState: {},
|
||||
}
|
||||
afterEach(() => fetchMock.reset());
|
||||
|
||||
describe('fetch failed', () => {
|
||||
beforeEach(async () => {
|
||||
const schemasAPILatestMock = fetchMock.getOnce(schemasAPILatestUrl, 404);
|
||||
const schemasAPIVersionsMock = fetchMock.getOnce(
|
||||
schemasAPIVersionsUrl,
|
||||
404
|
||||
);
|
||||
renderComponent();
|
||||
await waitFor(() => {
|
||||
expect(schemasAPILatestMock.called()).toBeTruthy();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(schemasAPIVersionsMock.called()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders pageloader', () => {
|
||||
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('Remove Schema')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('for a loaded scheme', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<Route path={clusterSchemaPath(':clusterName', ':subject')}>
|
||||
<Details />
|
||||
</Route>,
|
||||
{
|
||||
pathname: clusterSchemaPath(clusterName, subject),
|
||||
preloadedState: {
|
||||
loader: {
|
||||
'schemas/fetch': 'fulfilled',
|
||||
},
|
||||
schemas: schemasFulfilledState,
|
||||
},
|
||||
}
|
||||
);
|
||||
describe('fetch success', () => {
|
||||
describe('has schema versions', () => {
|
||||
beforeEach(async () => {
|
||||
const schemasAPILatestMock = fetchMock.getOnce(
|
||||
schemasAPILatestUrl,
|
||||
schemaVersion
|
||||
);
|
||||
const schemasAPIVersionsMock = fetchMock.getOnce(
|
||||
schemasAPIVersionsUrl,
|
||||
versionPayload
|
||||
);
|
||||
renderComponent();
|
||||
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', () => {
|
||||
expect(screen.getByText('Edit Schema')).toBeInTheDocument();
|
||||
});
|
||||
describe('empty schema versions', () => {
|
||||
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', () => {
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('table')).not.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();
|
||||
// seems like incorrect behaviour
|
||||
it('renders versions table with 0 items', () => {
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
expect(screen.getByText('No active Schema')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,47 +1,36 @@
|
|||
import React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import LatestVersionItem from 'components/Schemas/Details/LatestVersion/LatestVersionItem';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import theme from 'theme/theme';
|
||||
import { SchemaSubject } from 'generated-sources';
|
||||
import { render } from 'lib/testHelpers';
|
||||
import { screen } from '@testing-library/react';
|
||||
|
||||
import { jsonSchema, protoSchema } from './fixtures';
|
||||
|
||||
const renderComponent = (schema: SchemaSubject) => {
|
||||
render(<LatestVersionItem schema={schema} />);
|
||||
};
|
||||
|
||||
describe('LatestVersionItem', () => {
|
||||
it('renders latest version of json schema', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={theme}>
|
||||
<LatestVersionItem schema={jsonSchema} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('div[data-testid="meta-data"]').length).toEqual(1);
|
||||
expect(
|
||||
wrapper.find('div[data-testid="meta-data"] > div:first-child > p').text()
|
||||
).toEqual('1');
|
||||
expect(wrapper.exists('EditorViewer')).toBeTruthy();
|
||||
renderComponent(jsonSchema);
|
||||
expect(screen.getByText('Relevant version')).toBeInTheDocument();
|
||||
expect(screen.getByText('Latest version')).toBeInTheDocument();
|
||||
expect(screen.getByText('ID')).toBeInTheDocument();
|
||||
expect(screen.getByText('Subject')).toBeInTheDocument();
|
||||
expect(screen.getByText('Compatibility')).toBeInTheDocument();
|
||||
expect(screen.getByText('15')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('json-viewer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders latest version of compatibility', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={theme}>
|
||||
<LatestVersionItem schema={protoSchema} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
renderComponent(protoSchema);
|
||||
expect(screen.getByText('Relevant version')).toBeInTheDocument();
|
||||
expect(screen.getByText('Latest version')).toBeInTheDocument();
|
||||
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(
|
||||
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();
|
||||
expect(screen.getByText('BACKWARD')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('json-viewer')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,36 +1,26 @@
|
|||
import React from 'react';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import SchemaVersion from 'components/Schemas/Details/SchemaVersion/SchemaVersion';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import theme from 'theme/theme';
|
||||
import { render } from 'lib/testHelpers';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { versions } from './fixtures';
|
||||
|
||||
const renderComponent = () => {
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<SchemaVersion version={versions[0]} />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
describe('SchemaVersion', () => {
|
||||
it('renders versions', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={theme}>
|
||||
<table>
|
||||
<tbody>
|
||||
<SchemaVersion version={versions[0]} />
|
||||
</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();
|
||||
renderComponent();
|
||||
expect(screen.getAllByRole('cell')).toHaveLength(3);
|
||||
expect(screen.queryByTestId('json-viewer')).not.toBeInTheDocument();
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
expect(screen.getByTestId('json-viewer')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
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 = {
|
||||
subject: 'test',
|
||||
version: '1',
|
||||
version: '15',
|
||||
id: 1,
|
||||
schema:
|
||||
'{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
||||
|
@ -19,33 +28,3 @@ export const protoSchema: SchemaSubject = {
|
|||
compatibilityLevel: 'BACKWARD',
|
||||
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 {
|
||||
schemaAdded,
|
||||
schemasApiClient,
|
||||
fetchLatestSchema,
|
||||
getSchemaLatest,
|
||||
SCHEMA_LATEST_FETCH_ACTION,
|
||||
getAreSchemaLatestFulfilled,
|
||||
schemaUpdated,
|
||||
selectSchemaById,
|
||||
} from 'redux/reducers/schemas/schemasSlice';
|
||||
import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
|
||||
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';
|
||||
|
||||
|
@ -37,7 +42,15 @@ const Edit: React.FC = () => {
|
|||
handleSubmit,
|
||||
} = 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(() => {
|
||||
return schema?.schemaType === SchemaType.PROTOBUF
|
||||
|
@ -84,8 +97,9 @@ const Edit: React.FC = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
if (!schema) return null;
|
||||
|
||||
if (!isFetched || !schema) {
|
||||
return <PageLoader />;
|
||||
}
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<PageHeading text="Edit schema" />
|
||||
|
|
|
@ -3,49 +3,77 @@ import Edit from 'components/Schemas/Edit/Edit';
|
|||
import { render } from 'lib/testHelpers';
|
||||
import { clusterSchemaEditPath } from 'lib/paths';
|
||||
import {
|
||||
schemasFulfilledState,
|
||||
schemasInitialState,
|
||||
schemaVersion,
|
||||
} from 'redux/reducers/schemas/__test__/fixtures';
|
||||
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 { subject } = schemaVersion;
|
||||
const clusterName = 'testClusterName';
|
||||
const schemasAPILatestUrl = `/api/clusters/${clusterName}/schemas/${schemaVersion.subject}/latest`;
|
||||
|
||||
describe('Edit Component', () => {
|
||||
describe('schema exists', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<Route path={clusterSchemaEditPath(':clusterName', ':subject')}>
|
||||
<Edit />
|
||||
</Route>,
|
||||
{
|
||||
pathname: clusterSchemaEditPath(clusterName, subject),
|
||||
preloadedState: { schemas: schemasFulfilledState },
|
||||
}
|
||||
);
|
||||
const renderComponent = (
|
||||
initialState: RootState['schemas'] = schemasInitialState,
|
||||
context: ContextProps = contextInitialValue
|
||||
) => {
|
||||
return render(
|
||||
<Route path={clusterSchemaEditPath(':clusterName', ':subject')}>
|
||||
<ClusterContext.Provider value={context}>
|
||||
<Edit />
|
||||
</ClusterContext.Provider>
|
||||
</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', () => {
|
||||
expect(screen.getByText('Edit schema')).toBeInTheDocument();
|
||||
it('renders pageloader', () => {
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||
expect(screen.queryByText(schemaVersion.subject)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Submit')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('schema does not exist', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<Route path={clusterSchemaEditPath(':clusterName', ':subject')}>
|
||||
<Edit />
|
||||
</Route>,
|
||||
{
|
||||
pathname: clusterSchemaEditPath(clusterName, 'fake'),
|
||||
preloadedState: { schemas: schemasFulfilledState },
|
||||
}
|
||||
);
|
||||
});
|
||||
describe('fetch success', () => {
|
||||
describe('has schema versions', () => {
|
||||
beforeEach(async () => {
|
||||
const schemasAPILatestMock = fetchMock.getOnce(
|
||||
schemasAPILatestUrl,
|
||||
schemaVersion
|
||||
);
|
||||
|
||||
it('renders component', () => {
|
||||
expect(screen.queryByText('Edit schema')).not.toBeInTheDocument();
|
||||
renderComponent();
|
||||
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 { getResponse } from 'lib/errorHandling';
|
||||
import { useAppDispatch } from 'lib/hooks/redux';
|
||||
import usePagination from 'lib/hooks/usePagination';
|
||||
import useSearch from 'lib/hooks/useSearch';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
|
||||
|
@ -16,6 +18,9 @@ import * as S from './GlobalSchemaSelector.styled';
|
|||
const GlobalSchemaSelector: React.FC = () => {
|
||||
const { clusterName } = useParams<{ clusterName: string }>();
|
||||
const dispatch = useAppDispatch();
|
||||
const [searchText] = useSearch();
|
||||
const { page, perPage } = usePagination();
|
||||
|
||||
const [currentCompatibilityLevel, setCurrentCompatibilityLevel] =
|
||||
React.useState<CompatibilityLevelCompatibilityEnum | undefined>();
|
||||
const [nextCompatibilityLevel, setNextCompatibilityLevel] = React.useState<
|
||||
|
@ -61,7 +66,9 @@ const GlobalSchemaSelector: React.FC = () => {
|
|||
setCurrentCompatibilityLevel(nextCompatibilityLevel);
|
||||
setNextCompatibilityLevel(undefined);
|
||||
setIsConfirmationVisible(false);
|
||||
dispatch(fetchSchemas(clusterName));
|
||||
dispatch(
|
||||
fetchSchemas({ clusterName, page, perPage, search: searchText })
|
||||
);
|
||||
} catch (e) {
|
||||
const err = await getResponse(e as Response);
|
||||
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 { Button } from 'components/common/Button/Button';
|
||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||
import { useAppSelector } from 'lib/hooks/redux';
|
||||
import { selectAllSchemas } from 'redux/reducers/schemas/schemasSlice';
|
||||
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
||||
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 GlobalSchemaSelector from './GlobalSchemaSelector/GlobalSchemaSelector';
|
||||
|
||||
const List: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isReadOnly } = React.useContext(ClusterContext);
|
||||
const { clusterName } = useParams<{ clusterName: string }>();
|
||||
|
||||
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 (
|
||||
<>
|
||||
|
@ -34,28 +60,42 @@ const List: React.FC = () => {
|
|||
</>
|
||||
)}
|
||||
</PageHeading>
|
||||
<C.Table isFullwidth>
|
||||
<thead>
|
||||
<tr>
|
||||
<TableHeaderCell title="Schema Name" />
|
||||
<TableHeaderCell title="Version" />
|
||||
<TableHeaderCell title="Compatibility" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{schemas.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={10}>No schemas found</td>
|
||||
</tr>
|
||||
)}
|
||||
{schemas.map((subject) => (
|
||||
<ListItem
|
||||
key={[subject.id, subject.subject].join('-')}
|
||||
subject={subject}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</C.Table>
|
||||
<ControlPanelWrapper hasInput>
|
||||
<Search
|
||||
placeholder="Search by Schema Name"
|
||||
value={searchText}
|
||||
handleSearch={handleSearchText}
|
||||
/>
|
||||
</ControlPanelWrapper>
|
||||
{isFetched ? (
|
||||
<>
|
||||
<C.Table isFullwidth>
|
||||
<thead>
|
||||
<tr>
|
||||
<TableHeaderCell title="Schema Name" />
|
||||
<TableHeaderCell title="Version" />
|
||||
<TableHeaderCell title="Compatibility" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{schemas.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={10}>No schemas found</td>
|
||||
</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 { Route } from 'react-router';
|
||||
import { clusterSchemasPath } from 'lib/paths';
|
||||
import { screen } from '@testing-library/dom';
|
||||
import { screen, waitFor } from '@testing-library/dom';
|
||||
import {
|
||||
schemasFulfilledState,
|
||||
schemasInitialState,
|
||||
schemaVersion1,
|
||||
schemaVersion2,
|
||||
} from 'redux/reducers/schemas/__test__/fixtures';
|
||||
import ClusterContext, {
|
||||
ContextProps,
|
||||
initialValue as contextInitialValue,
|
||||
} from 'components/contexts/ClusterContext';
|
||||
import { RootState } from 'redux/interfaces';
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import { schemasPayload, schemasEmptyPayload } from './fixtures';
|
||||
|
||||
const clusterName = 'testClusterName';
|
||||
|
||||
const schemasAPIUrl = `/api/clusters/${clusterName}/schemas`;
|
||||
const schemasAPICompabilityUrl = `${schemasAPIUrl}/compatibility`;
|
||||
const renderComponent = (
|
||||
initialState: RootState['schemas'] = schemasInitialState,
|
||||
context: ContextProps = contextInitialValue
|
||||
) =>
|
||||
render(
|
||||
<ClusterContext.Provider value={context}>
|
||||
<Route path={clusterSchemasPath(':clusterName')}>
|
||||
<Route path={clusterSchemasPath(':clusterName')}>
|
||||
<ClusterContext.Provider value={context}>
|
||||
<List />
|
||||
</Route>
|
||||
</ClusterContext.Provider>,
|
||||
</ClusterContext.Provider>
|
||||
</Route>,
|
||||
{
|
||||
pathname: clusterSchemasPath(clusterName),
|
||||
preloadedState: {
|
||||
loader: {
|
||||
'schemas/fetch': 'fulfilled',
|
||||
},
|
||||
schemas: initialState,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
describe('List', () => {
|
||||
it('renders list', () => {
|
||||
renderComponent(schemasFulfilledState);
|
||||
expect(screen.getByText('MySchemaSubject')).toBeInTheDocument();
|
||||
expect(screen.getByText('schema7_1')).toBeInTheDocument();
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
it('renders empty table', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText('No schemas found')).toBeInTheDocument();
|
||||
describe('fetch error', () => {
|
||||
it('shows progressbar', async () => {
|
||||
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', () => {
|
||||
it('does not render Create Schema button', () => {
|
||||
renderComponent(schemasFulfilledState, {
|
||||
...contextInitialValue,
|
||||
isReadOnly: true,
|
||||
describe('fetch success', () => {
|
||||
describe('responded without schemas', () => {
|
||||
beforeEach(async () => {
|
||||
const fetchSchemasMock = fetchMock.getOnce(
|
||||
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[] = [
|
||||
{
|
||||
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: '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,
|
||||
},
|
||||
];
|
||||
export const schemas = [schemaVersion1, schemaVersion2];
|
||||
|
||||
export const schemasPayload = {
|
||||
pageCount: 1,
|
||||
schemas,
|
||||
};
|
||||
|
||||
export const schemasEmptyPayload = {
|
||||
pageCount: 1,
|
||||
schemas: [],
|
||||
};
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
import React from 'react';
|
||||
import { Switch, useParams } from 'react-router-dom';
|
||||
import { Switch } from 'react-router-dom';
|
||||
import {
|
||||
clusterSchemaNewPath,
|
||||
clusterSchemaPath,
|
||||
clusterSchemaEditPath,
|
||||
clusterSchemasPath,
|
||||
} 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 Details from 'components/Schemas/Details/Details';
|
||||
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';
|
||||
|
||||
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 (
|
||||
<Switch>
|
||||
<BreadcrumbRoute
|
||||
|
|
|
@ -3,6 +3,7 @@ import usePagination from 'lib/hooks/usePagination';
|
|||
import { range } from 'lodash';
|
||||
import React from 'react';
|
||||
import PageControl from 'components/common/Pagination/PageControl';
|
||||
import useSearch from 'lib/hooks/useSearch';
|
||||
|
||||
import * as S from './Pagination.styled';
|
||||
|
||||
|
@ -14,12 +15,17 @@ const NEIGHBOURS = 2;
|
|||
|
||||
const Pagination: React.FC<PaginationProps> = ({ totalPages }) => {
|
||||
const { page, perPage, pathname } = usePagination();
|
||||
const [searchText] = useSearch();
|
||||
|
||||
const currentPage = page || 1;
|
||||
const currentPerPage = perPage || PER_PAGE;
|
||||
|
||||
const searchParam = searchText ? `&q=${searchText}` : '';
|
||||
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(() => {
|
||||
// Total visible numbers: neighbours, current, first & last
|
||||
|
|
|
@ -17,6 +17,7 @@ const Search: React.FC<SearchProps> = ({
|
|||
(e) => handleSearch(e.target.value),
|
||||
300
|
||||
);
|
||||
|
||||
return (
|
||||
<Input
|
||||
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',
|
||||
version: '1',
|
||||
id: 2,
|
||||
|
@ -9,31 +21,41 @@ export const schemaVersion = {
|
|||
compatibilityLevel: 'FULL',
|
||||
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 = {
|
||||
ids: ['MySchemaSubject', 'schema7_1'],
|
||||
totalPages: 1,
|
||||
ids: [schemaVersion2.subject, schemaVersion1.subject],
|
||||
entities: {
|
||||
MySchemaSubject: {
|
||||
subject: 'MySchemaSubject',
|
||||
version: '1',
|
||||
id: 28,
|
||||
schema: '12',
|
||||
compatibilityLevel: 'FORWARD_TRANSITIVE',
|
||||
schemaType: SchemaType.JSON,
|
||||
},
|
||||
schema7_1: schemaVersion,
|
||||
[schemaVersion2.subject]: schemaVersion2,
|
||||
[schemaVersion1.subject]: schemaVersion1,
|
||||
},
|
||||
versions: {
|
||||
latest: null,
|
||||
ids: [],
|
||||
entities: {},
|
||||
},
|
||||
};
|
||||
|
||||
export const schemasInitialState = {
|
||||
export const versionFulfilledState = {
|
||||
totalPages: 1,
|
||||
ids: [],
|
||||
entities: {},
|
||||
versions: {
|
||||
ids: [],
|
||||
entities: {},
|
||||
latest: schemaVersion2,
|
||||
ids: [schemaVersion1.id, schemaVersion2.id],
|
||||
entities: {
|
||||
[schemaVersion2.id]: schemaVersion2,
|
||||
[schemaVersion1.id]: schemaVersion1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -4,7 +4,14 @@ import {
|
|||
createSelector,
|
||||
createSlice,
|
||||
} 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 { getResponse } from 'lib/errorHandling';
|
||||
import { ClusterName, RootState } from 'redux/interfaces';
|
||||
|
@ -13,21 +20,44 @@ import { createFetchingSelector } from 'redux/reducers/loader/selectors';
|
|||
const apiClientConf = new Configuration(BASE_PARAMS);
|
||||
export const schemasApiClient = new SchemasApi(apiClientConf);
|
||||
|
||||
export const fetchSchemas = createAsyncThunk<SchemaSubject[], ClusterName>(
|
||||
'schemas/fetch',
|
||||
async (clusterName: ClusterName, { rejectWithValue }) => {
|
||||
export const SCHEMA_LATEST_FETCH_ACTION = 'schemas/latest/fetch';
|
||||
export const fetchLatestSchema = createAsyncThunk<
|
||||
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 {
|
||||
return await schemasApiClient.getSchemas({ clusterName });
|
||||
return await schemasApiClient.getSchemas({
|
||||
clusterName,
|
||||
page,
|
||||
perPage,
|
||||
search: search || undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
return rejectWithValue(await getResponse(error as Response));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const SCHEMAS_VERSIONS_FETCH_ACTION = 'schemas/versions/fetch';
|
||||
export const fetchSchemaVersions = createAsyncThunk<
|
||||
SchemaSubject[],
|
||||
{ clusterName: ClusterName; subject: SchemaSubject['subject'] }
|
||||
>(
|
||||
'schemas/versions/fetch',
|
||||
SCHEMAS_VERSIONS_FETCH_ACTION,
|
||||
async ({ clusterName, subject }, { rejectWithValue }) => {
|
||||
try {
|
||||
return await schemasApiClient.getAllVersionsBySubject({
|
||||
|
@ -48,18 +78,31 @@ const schemasAdapter = createEntityAdapter<SchemaSubject>({
|
|||
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({
|
||||
name: 'schemas',
|
||||
initialState: schemasAdapter.getInitialState({
|
||||
versions: schemaVersionsAdapter.getInitialState(),
|
||||
}),
|
||||
initialState,
|
||||
reducers: {
|
||||
schemaAdded: schemasAdapter.addOne,
|
||||
schemaUpdated: schemasAdapter.upsertOne,
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
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 }) => {
|
||||
schemaVersionsAdapter.setAll(state.versions, payload);
|
||||
|
@ -70,19 +113,32 @@ const schemasSlice = createSlice({
|
|||
export const { selectAll: selectAllSchemas, selectById: selectSchemaById } =
|
||||
schemasAdapter.getSelectors<RootState>((state) => state.schemas);
|
||||
|
||||
export const { selectAll: selectAllSchemaVersions } =
|
||||
schemaVersionsAdapter.getSelectors<RootState>(
|
||||
(state) => state.schemas.versions
|
||||
);
|
||||
export const {
|
||||
selectAll: selectAllSchemaVersions,
|
||||
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 getAreSchemasFulfilled = createSelector(
|
||||
createFetchingSelector('schemas/fetch'),
|
||||
createFetchingSelector(SCHEMAS_FETCH_ACTION),
|
||||
(status) => status === 'fulfilled'
|
||||
);
|
||||
|
||||
export const getAreSchemaLatestFulfilled = createSelector(
|
||||
createFetchingSelector(SCHEMA_LATEST_FETCH_ACTION),
|
||||
(status) => status === 'fulfilled'
|
||||
);
|
||||
export const getAreSchemaVersionsFulfilled = createSelector(
|
||||
createFetchingSelector('schemas/versions/fetch'),
|
||||
createFetchingSelector(SCHEMAS_VERSIONS_FETCH_ACTION),
|
||||
(status) => status === 'fulfilled'
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue