mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-13 01:38:13 +00:00
GEN 16908 - Support pagination for children field (#19650)
* GEN 16908 - Support pagination for children field * Fix tests - Support pagination for children field * move children pagination listing to separate api * added pagination support from UI * added playwright test for the pagination test --------- Co-authored-by: Ashish Gupta <ashish@getcollate.io>
This commit is contained in:
parent
88f615ae47
commit
aefc36b596
@ -1019,6 +1019,26 @@ public interface CollectionDAO {
|
|||||||
@Bind("relation") int relation,
|
@Bind("relation") int relation,
|
||||||
@Bind("toEntity") String toEntity);
|
@Bind("toEntity") String toEntity);
|
||||||
|
|
||||||
|
@SqlQuery(
|
||||||
|
"SELECT COUNT(toId) FROM entity_relationship WHERE fromId = :fromId AND fromEntity = :fromEntity "
|
||||||
|
+ "AND relation IN (<relation>)")
|
||||||
|
@RegisterRowMapper(ToRelationshipMapper.class)
|
||||||
|
int countFindTo(
|
||||||
|
@BindUUID("fromId") UUID fromId,
|
||||||
|
@Bind("fromEntity") String fromEntity,
|
||||||
|
@BindList("relation") List<Integer> relation);
|
||||||
|
|
||||||
|
@SqlQuery(
|
||||||
|
"SELECT toId, toEntity, json FROM entity_relationship WHERE fromId = :fromId AND fromEntity = :fromEntity "
|
||||||
|
+ "AND relation IN (<relation>) ORDER BY toId LIMIT :limit OFFSET :offset")
|
||||||
|
@RegisterRowMapper(ToRelationshipMapper.class)
|
||||||
|
List<EntityRelationshipRecord> findToWithOffset(
|
||||||
|
@BindUUID("fromId") UUID fromId,
|
||||||
|
@Bind("fromEntity") String fromEntity,
|
||||||
|
@BindList("relation") List<Integer> relation,
|
||||||
|
@Bind("offset") int offset,
|
||||||
|
@Bind("limit") int limit);
|
||||||
|
|
||||||
@ConnectionAwareSqlQuery(
|
@ConnectionAwareSqlQuery(
|
||||||
value =
|
value =
|
||||||
"SELECT toId, toEntity, json FROM entity_relationship "
|
"SELECT toId, toEntity, json FROM entity_relationship "
|
||||||
|
@ -8,6 +8,7 @@ import static org.openmetadata.service.Entity.FIELD_PARENT;
|
|||||||
import static org.openmetadata.service.Entity.FIELD_TAGS;
|
import static org.openmetadata.service.Entity.FIELD_TAGS;
|
||||||
import static org.openmetadata.service.Entity.STORAGE_SERVICE;
|
import static org.openmetadata.service.Entity.STORAGE_SERVICE;
|
||||||
import static org.openmetadata.service.Entity.populateEntityFieldTags;
|
import static org.openmetadata.service.Entity.populateEntityFieldTags;
|
||||||
|
import static org.openmetadata.service.util.EntityUtil.getEntityReferences;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -33,6 +34,7 @@ import org.openmetadata.service.resources.storages.ContainerResource;
|
|||||||
import org.openmetadata.service.util.EntityUtil;
|
import org.openmetadata.service.util.EntityUtil;
|
||||||
import org.openmetadata.service.util.FullyQualifiedName;
|
import org.openmetadata.service.util.FullyQualifiedName;
|
||||||
import org.openmetadata.service.util.JsonUtils;
|
import org.openmetadata.service.util.JsonUtils;
|
||||||
|
import org.openmetadata.service.util.ResultList;
|
||||||
|
|
||||||
public class ContainerRepository extends EntityRepository<Container> {
|
public class ContainerRepository extends EntityRepository<Container> {
|
||||||
private static final String CONTAINER_UPDATE_FIELDS = "dataModel";
|
private static final String CONTAINER_UPDATE_FIELDS = "dataModel";
|
||||||
@ -223,6 +225,49 @@ public class ContainerRepository extends EntityRepository<Container> {
|
|||||||
return super.getTaskWorkflow(threadContext);
|
return super.getTaskWorkflow(threadContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ResultList<Container> listChildren(String parentFQN, Integer limit, Integer offset) {
|
||||||
|
|
||||||
|
Container parentContainer = dao.findEntityByName(parentFQN);
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<CollectionDAO.EntityRelationshipRecord> relationshipRecords =
|
||||||
|
daoCollection
|
||||||
|
.relationshipDAO()
|
||||||
|
.findToWithOffset(
|
||||||
|
parentContainer.getId(),
|
||||||
|
CONTAINER,
|
||||||
|
List.of(Relationship.CONTAINS.ordinal()),
|
||||||
|
offset,
|
||||||
|
limit);
|
||||||
|
|
||||||
|
int total =
|
||||||
|
daoCollection
|
||||||
|
.relationshipDAO()
|
||||||
|
.countFindTo(
|
||||||
|
parentContainer.getId(), CONTAINER, List.of(Relationship.CONTAINS.ordinal()));
|
||||||
|
|
||||||
|
if (relationshipRecords.isEmpty()) {
|
||||||
|
return new ResultList<>(new ArrayList<>(), null, null, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<EntityReference> refs = getEntityReferences(relationshipRecords);
|
||||||
|
List<Container> children = new ArrayList<>();
|
||||||
|
|
||||||
|
for (EntityReference ref : refs) {
|
||||||
|
Container container =
|
||||||
|
Entity.getEntity(ref, EntityUtil.Fields.EMPTY_FIELDS.toString(), Include.ALL);
|
||||||
|
children.add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ResultList<>(children, null, null, total);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
String.format(
|
||||||
|
"Failed to fetch children for container [%s]: %s", parentFQN, e.getMessage()),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static class DataModelDescriptionTaskWorkflow extends DescriptionTaskWorkflow {
|
static class DataModelDescriptionTaskWorkflow extends DescriptionTaskWorkflow {
|
||||||
private final Column column;
|
private final Column column;
|
||||||
|
|
||||||
|
@ -546,4 +546,39 @@ public class ContainerResource extends EntityResource<Container, ContainerReposi
|
|||||||
@Valid RestoreEntity restore) {
|
@Valid RestoreEntity restore) {
|
||||||
return restoreEntity(uriInfo, securityContext, restore.getId());
|
return restoreEntity(uriInfo, securityContext, restore.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/name/{fqn}/children")
|
||||||
|
@Operation(
|
||||||
|
operationId = "listContainerChildren",
|
||||||
|
summary = "List children containers",
|
||||||
|
description = "Get a list of children containers with pagination.",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "List of children containers",
|
||||||
|
content =
|
||||||
|
@Content(
|
||||||
|
mediaType = "application/json",
|
||||||
|
schema = @Schema(implementation = ContainerList.class)))
|
||||||
|
})
|
||||||
|
public ResultList<Container> listChildren(
|
||||||
|
@Context UriInfo uriInfo,
|
||||||
|
@Context SecurityContext securityContext,
|
||||||
|
@Parameter(description = "Fully qualified name of the container") @PathParam("fqn")
|
||||||
|
String fqn,
|
||||||
|
@Parameter(
|
||||||
|
description = "Limit the number of children returned. (1 to 1000000, default = 10)")
|
||||||
|
@DefaultValue("10")
|
||||||
|
@Min(0)
|
||||||
|
@Max(1000000)
|
||||||
|
@QueryParam("limit")
|
||||||
|
Integer limit,
|
||||||
|
@Parameter(description = "Returns list of children after the given offset")
|
||||||
|
@DefaultValue("0")
|
||||||
|
@QueryParam("offset")
|
||||||
|
@Min(0)
|
||||||
|
Integer offset) {
|
||||||
|
return repository.listChildren(fqn, limit, offset);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import javax.ws.rs.client.WebTarget;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.Response.Status;
|
import javax.ws.rs.core.Response.Status;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -394,6 +395,7 @@ public class ContainerResourceTest extends EntityResourceTest<Container, CreateC
|
|||||||
.withOwners(List.of(DATA_CONSUMER.getEntityReference()))
|
.withOwners(List.of(DATA_CONSUMER.getEntityReference()))
|
||||||
.withSize(0.0);
|
.withSize(0.0);
|
||||||
Container rootContainer = createAndCheckEntity(createRootContainer, ADMIN_AUTH_HEADERS);
|
Container rootContainer = createAndCheckEntity(createRootContainer, ADMIN_AUTH_HEADERS);
|
||||||
|
String rootContainerFQN = rootContainer.getFullyQualifiedName();
|
||||||
|
|
||||||
CreateContainer createChildOneContainer =
|
CreateContainer createChildOneContainer =
|
||||||
new CreateContainer()
|
new CreateContainer()
|
||||||
@ -485,6 +487,19 @@ public class ContainerResourceTest extends EntityResourceTest<Container, CreateC
|
|||||||
ResultList<Container> rootContainerList = listEntities(queryParams, ADMIN_AUTH_HEADERS);
|
ResultList<Container> rootContainerList = listEntities(queryParams, ADMIN_AUTH_HEADERS);
|
||||||
assertEquals(1, rootContainerList.getData().size());
|
assertEquals(1, rootContainerList.getData().size());
|
||||||
assertEquals("s3.0_root", rootContainerList.getData().get(0).getFullyQualifiedName());
|
assertEquals("s3.0_root", rootContainerList.getData().get(0).getFullyQualifiedName());
|
||||||
|
|
||||||
|
// Test paginated child container list
|
||||||
|
ResultList<Container> children = getContainerChildren(rootContainerFQN, null, null);
|
||||||
|
assertEquals(2, children.getData().size());
|
||||||
|
|
||||||
|
ResultList<Container> childrenWithLimit = getContainerChildren(rootContainerFQN, 5, 0);
|
||||||
|
assertEquals(2, childrenWithLimit.getData().size());
|
||||||
|
|
||||||
|
ResultList<Container> childrenWithOffset = getContainerChildren(rootContainerFQN, 1, 1);
|
||||||
|
assertEquals(1, childrenWithOffset.getData().size());
|
||||||
|
|
||||||
|
ResultList<Container> childrenWithLargeOffset = getContainerChildren(rootContainerFQN, 1, 3);
|
||||||
|
assertTrue(childrenWithLargeOffset.getData().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -696,6 +711,14 @@ public class ContainerResourceTest extends EntityResourceTest<Container, CreateC
|
|||||||
createdEntity.getFullyQualifiedName());
|
createdEntity.getFullyQualifiedName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ResultList<Container> getContainerChildren(String fqn, Integer limit, Integer offset)
|
||||||
|
throws HttpResponseException {
|
||||||
|
WebTarget target = getResource(String.format("containers/name/%s/children", fqn));
|
||||||
|
target = limit != null ? target.queryParam("limit", limit) : target;
|
||||||
|
target = offset != null ? target.queryParam("offset", offset) : target;
|
||||||
|
return TestUtils.get(target, ContainerList.class, ADMIN_AUTH_HEADERS);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testInheritedPermissionFromParent(TestInfo test) throws IOException {
|
void testInheritedPermissionFromParent(TestInfo test) throws IOException {
|
||||||
// Create a storage service with owner data consumer
|
// Create a storage service with owner data consumer
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Collate.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import { uuid } from '../utils/common';
|
||||||
|
|
||||||
|
export const CONTAINER_CHILDREN = Array.from({ length: 25 }, (_, i) => {
|
||||||
|
const id = uuid();
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: `pw-container-children${i + 1}-${id}`,
|
||||||
|
displayName: `pw-container-children-${i + 1}-${id}`,
|
||||||
|
};
|
||||||
|
});
|
@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Collate.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import test, { expect } from '@playwright/test';
|
||||||
|
import { CONTAINER_CHILDREN } from '../../constant/contianer';
|
||||||
|
import { ContainerClass } from '../../support/entity/ContainerClass';
|
||||||
|
import { createNewPage, redirectToHomePage } from '../../utils/common';
|
||||||
|
|
||||||
|
// use the admin user to login
|
||||||
|
test.use({ storageState: 'playwright/.auth/admin.json' });
|
||||||
|
|
||||||
|
const container = new ContainerClass();
|
||||||
|
|
||||||
|
test.slow(true);
|
||||||
|
|
||||||
|
test.describe('Container entity specific tests ', () => {
|
||||||
|
test.beforeAll('Setup pre-requests', async ({ browser }) => {
|
||||||
|
const { afterAction, apiContext } = await createNewPage(browser);
|
||||||
|
|
||||||
|
await container.create(apiContext, CONTAINER_CHILDREN);
|
||||||
|
|
||||||
|
await afterAction();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll('Clean up', async ({ browser }) => {
|
||||||
|
const { afterAction, apiContext } = await createNewPage(browser);
|
||||||
|
|
||||||
|
await container.delete(apiContext);
|
||||||
|
|
||||||
|
await afterAction();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.beforeEach('Visit home page', async ({ page }) => {
|
||||||
|
await redirectToHomePage(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Container page children pagination', async ({ page }) => {
|
||||||
|
await container.visitEntityPage(page);
|
||||||
|
|
||||||
|
await page.getByText('Children').click();
|
||||||
|
|
||||||
|
await expect(page.getByTestId('pagination')).toBeVisible();
|
||||||
|
await expect(page.getByTestId('previous')).toBeDisabled();
|
||||||
|
await expect(page.getByTestId('page-indicator')).toContainText('1/2 Page');
|
||||||
|
|
||||||
|
// Check the second page pagination
|
||||||
|
const childrenResponse = page.waitForResponse(
|
||||||
|
'/api/v1/containers/name/*/children?limit=15&offset=15'
|
||||||
|
);
|
||||||
|
await page.getByTestId('next').click();
|
||||||
|
await childrenResponse;
|
||||||
|
|
||||||
|
await expect(page.getByTestId('next')).toBeDisabled();
|
||||||
|
await expect(page.getByTestId('page-indicator')).toContainText('2/2 Page');
|
||||||
|
|
||||||
|
// Check around the page sizing change
|
||||||
|
await page.getByTestId('page-size-selection-dropdown').click();
|
||||||
|
|
||||||
|
const childrenResponseSizeChange = page.waitForResponse(
|
||||||
|
'/api/v1/containers/name/*/children?limit=25&offset=0'
|
||||||
|
);
|
||||||
|
await page.getByText('25 / Page').click();
|
||||||
|
await childrenResponseSizeChange;
|
||||||
|
|
||||||
|
await page.waitForSelector('.ant-spin', {
|
||||||
|
state: 'detached',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByTestId('next')).toBeDisabled();
|
||||||
|
await expect(page.getByTestId('previous')).toBeDisabled();
|
||||||
|
await expect(page.getByTestId('page-indicator')).toContainText('1/1 Page');
|
||||||
|
|
||||||
|
// Back to the original page size
|
||||||
|
await page.getByTestId('page-size-selection-dropdown').click();
|
||||||
|
|
||||||
|
const childrenResponseSizeChange2 = page.waitForResponse(
|
||||||
|
'/api/v1/containers/name/*/children?limit=15&offset=0'
|
||||||
|
);
|
||||||
|
await page.getByText('15 / Page').click();
|
||||||
|
await childrenResponseSizeChange2;
|
||||||
|
|
||||||
|
await page.waitForSelector('.ant-spin', {
|
||||||
|
state: 'detached',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByTestId('previous')).toBeDisabled();
|
||||||
|
await expect(page.getByTestId('page-indicator')).toContainText('1/2 Page');
|
||||||
|
});
|
||||||
|
});
|
@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
import { APIRequestContext, Page } from '@playwright/test';
|
import { APIRequestContext, Page } from '@playwright/test';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
|
import { isUndefined } from 'lodash';
|
||||||
import { SERVICE_TYPE } from '../../constant/service';
|
import { SERVICE_TYPE } from '../../constant/service';
|
||||||
import { ServiceTypes } from '../../constant/settings';
|
import { ServiceTypes } from '../../constant/settings';
|
||||||
import { uuid } from '../../utils/common';
|
import { uuid } from '../../utils/common';
|
||||||
@ -89,6 +90,7 @@ export class ContainerClass extends EntityClass {
|
|||||||
entityResponseData: ResponseDataWithServiceType =
|
entityResponseData: ResponseDataWithServiceType =
|
||||||
{} as ResponseDataWithServiceType;
|
{} as ResponseDataWithServiceType;
|
||||||
childResponseData: ResponseDataType = {} as ResponseDataType;
|
childResponseData: ResponseDataType = {} as ResponseDataType;
|
||||||
|
childArrayResponseData: ResponseDataType[] = [];
|
||||||
|
|
||||||
constructor(name?: string) {
|
constructor(name?: string) {
|
||||||
super(EntityTypeEndpoint.Container);
|
super(EntityTypeEndpoint.Container);
|
||||||
@ -98,7 +100,10 @@ export class ContainerClass extends EntityClass {
|
|||||||
this.serviceCategory = SERVICE_TYPE.Storage;
|
this.serviceCategory = SERVICE_TYPE.Storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(apiContext: APIRequestContext) {
|
async create(
|
||||||
|
apiContext: APIRequestContext,
|
||||||
|
customChildContainer?: { name: string; displayName: string }[]
|
||||||
|
) {
|
||||||
const serviceResponse = await apiContext.post(
|
const serviceResponse = await apiContext.post(
|
||||||
'/api/v1/services/storageServices',
|
'/api/v1/services/storageServices',
|
||||||
{
|
{
|
||||||
@ -112,6 +117,25 @@ export class ContainerClass extends EntityClass {
|
|||||||
this.serviceResponseData = await serviceResponse.json();
|
this.serviceResponseData = await serviceResponse.json();
|
||||||
this.entityResponseData = await entityResponse.json();
|
this.entityResponseData = await entityResponse.json();
|
||||||
|
|
||||||
|
if (!isUndefined(customChildContainer)) {
|
||||||
|
const childArrayResponseData: ResponseDataType[] = [];
|
||||||
|
for (const child of customChildContainer) {
|
||||||
|
const childContainer = {
|
||||||
|
...child,
|
||||||
|
service: this.service.name,
|
||||||
|
parent: {
|
||||||
|
id: this.entityResponseData.id,
|
||||||
|
type: 'container',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const childResponse = await apiContext.post('/api/v1/containers', {
|
||||||
|
data: childContainer,
|
||||||
|
});
|
||||||
|
|
||||||
|
childArrayResponseData.push(await childResponse.json());
|
||||||
|
}
|
||||||
|
this.childArrayResponseData = childArrayResponseData;
|
||||||
|
} else {
|
||||||
const childContainer = {
|
const childContainer = {
|
||||||
...this.childContainer,
|
...this.childContainer,
|
||||||
parent: {
|
parent: {
|
||||||
@ -125,6 +149,7 @@ export class ContainerClass extends EntityClass {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.childResponseData = await childResponse.json();
|
this.childResponseData = await childResponse.json();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
service: serviceResponse.body,
|
service: serviceResponse.body,
|
||||||
|
@ -10,12 +10,26 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import { pagingObject } from '../../../constants/constants';
|
||||||
import ContainerChildren from './ContainerChildren';
|
import ContainerChildren from './ContainerChildren';
|
||||||
|
|
||||||
|
jest.mock('../../common/NextPrevious/NextPrevious', () => {
|
||||||
|
return jest.fn().mockImplementation(({ pagingHandler, onShowSizeChange }) => (
|
||||||
|
<div>
|
||||||
|
<p>NextPreviousComponent</p>
|
||||||
|
<button onClick={pagingHandler}>childrenPageChangeButton</button>
|
||||||
|
<button onClick={onShowSizeChange}>pageSizeChangeButton</button>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
const mockFetchChildren = jest.fn();
|
const mockFetchChildren = jest.fn();
|
||||||
|
const mockHandleChildrenPageChange = jest.fn();
|
||||||
|
const mockHandlePageSizeChange = jest.fn();
|
||||||
|
|
||||||
const mockChildrenList = [
|
const mockChildrenList = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@ -36,6 +50,14 @@ const mockChildrenList = [
|
|||||||
const mockDataProps = {
|
const mockDataProps = {
|
||||||
childrenList: mockChildrenList,
|
childrenList: mockChildrenList,
|
||||||
fetchChildren: mockFetchChildren,
|
fetchChildren: mockFetchChildren,
|
||||||
|
pagingHookData: {
|
||||||
|
paging: pagingObject,
|
||||||
|
pageSize: 15,
|
||||||
|
currentPage: 1,
|
||||||
|
showPagination: true,
|
||||||
|
handleChildrenPageChange: mockHandleChildrenPageChange,
|
||||||
|
handlePageSizeChange: mockHandlePageSizeChange,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('ContainerChildren', () => {
|
describe('ContainerChildren', () => {
|
||||||
@ -70,6 +92,22 @@ describe('ContainerChildren', () => {
|
|||||||
expect(screen.getByText('label.description')).toBeInTheDocument();
|
expect(screen.getByText('label.description')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should not render pagination component when not visible', () => {
|
||||||
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<ContainerChildren
|
||||||
|
{...mockDataProps}
|
||||||
|
pagingHookData={{
|
||||||
|
...mockDataProps.pagingHookData,
|
||||||
|
showPagination: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('NextPreviousComponent')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('Should render container names as links', () => {
|
it('Should render container names as links', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
@ -108,4 +146,44 @@ describe('ContainerChildren', () => {
|
|||||||
expect(previewer).toHaveTextContent(mockChildrenList[index].description);
|
expect(previewer).toHaveTextContent(mockChildrenList[index].description);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should render pagination component when showPagination props is true', () => {
|
||||||
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<ContainerChildren
|
||||||
|
{...mockDataProps}
|
||||||
|
pagingHookData={{
|
||||||
|
...mockDataProps.pagingHookData,
|
||||||
|
showPagination: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('NextPreviousComponent')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should trigger handleChildrenPageChange hook prop on button click', () => {
|
||||||
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<ContainerChildren {...mockDataProps} />
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('childrenPageChangeButton'));
|
||||||
|
|
||||||
|
expect(mockHandleChildrenPageChange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should trigger handlePageSizeChange hook prop on button click', () => {
|
||||||
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<ContainerChildren {...mockDataProps} />
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('pageSizeChangeButton'));
|
||||||
|
|
||||||
|
expect(mockHandlePageSizeChange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { Typography } from 'antd';
|
import { Col, Row, Typography } from 'antd';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import React, { FC, useEffect, useMemo } from 'react';
|
import React, { FC, useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -19,23 +19,43 @@ import { getEntityDetailsPath } from '../../../constants/constants';
|
|||||||
import { EntityType } from '../../../enums/entity.enum';
|
import { EntityType } from '../../../enums/entity.enum';
|
||||||
import { Container } from '../../../generated/entity/data/container';
|
import { Container } from '../../../generated/entity/data/container';
|
||||||
import { EntityReference } from '../../../generated/type/entityReference';
|
import { EntityReference } from '../../../generated/type/entityReference';
|
||||||
|
import { Paging } from '../../../generated/type/paging';
|
||||||
import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils';
|
import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils';
|
||||||
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||||
|
import NextPrevious from '../../common/NextPrevious/NextPrevious';
|
||||||
|
import { PagingHandlerParams } from '../../common/NextPrevious/NextPrevious.interface';
|
||||||
import RichTextEditorPreviewerV1 from '../../common/RichTextEditor/RichTextEditorPreviewerV1';
|
import RichTextEditorPreviewerV1 from '../../common/RichTextEditor/RichTextEditorPreviewerV1';
|
||||||
import Table from '../../common/Table/Table';
|
import Table from '../../common/Table/Table';
|
||||||
|
|
||||||
interface ContainerChildrenProps {
|
interface ContainerChildrenProps {
|
||||||
childrenList: Container['children'];
|
childrenList: Container['children'];
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
pagingHookData: {
|
||||||
|
paging: Paging;
|
||||||
|
pageSize: number;
|
||||||
|
currentPage: number;
|
||||||
|
showPagination: boolean;
|
||||||
|
handleChildrenPageChange: (data: PagingHandlerParams) => void;
|
||||||
|
handlePageSizeChange: (page: number) => void;
|
||||||
|
};
|
||||||
fetchChildren: () => void;
|
fetchChildren: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContainerChildren: FC<ContainerChildrenProps> = ({
|
const ContainerChildren: FC<ContainerChildrenProps> = ({
|
||||||
childrenList,
|
childrenList,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
pagingHookData,
|
||||||
fetchChildren,
|
fetchChildren,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
paging,
|
||||||
|
pageSize,
|
||||||
|
currentPage,
|
||||||
|
showPagination,
|
||||||
|
handleChildrenPageChange,
|
||||||
|
handlePageSizeChange,
|
||||||
|
} = pagingHookData;
|
||||||
|
|
||||||
const columns: ColumnsType<EntityReference> = useMemo(
|
const columns: ColumnsType<EntityReference> = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@ -83,9 +103,11 @@ const ContainerChildren: FC<ContainerChildrenProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchChildren();
|
fetchChildren();
|
||||||
}, []);
|
}, [pageSize]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Row className="m-b-md" gutter={[0, 16]}>
|
||||||
|
<Col span={24}>
|
||||||
<Table
|
<Table
|
||||||
bordered
|
bordered
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@ -99,6 +121,21 @@ const ContainerChildren: FC<ContainerChildrenProps> = ({
|
|||||||
rowKey="id"
|
rowKey="id"
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
{showPagination && (
|
||||||
|
<NextPrevious
|
||||||
|
isNumberBased
|
||||||
|
currentPage={currentPage}
|
||||||
|
isLoading={isLoading}
|
||||||
|
pageSize={pageSize}
|
||||||
|
paging={paging}
|
||||||
|
pagingHandler={handleChildrenPageChange}
|
||||||
|
onShowSizeChange={handlePageSizeChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,3 +19,7 @@ export type ListParams = {
|
|||||||
after?: string;
|
after?: string;
|
||||||
include?: Include;
|
include?: Include;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ListParamsWithOffset = ListParams & {
|
||||||
|
offset?: number;
|
||||||
|
};
|
||||||
|
@ -184,6 +184,17 @@ jest.mock('../../utils/CommonUtils', () => ({
|
|||||||
sortTagsCaseInsensitive: jest.fn().mockImplementation((tags) => tags),
|
sortTagsCaseInsensitive: jest.fn().mockImplementation((tags) => tags),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../hooks/paging/usePaging', () => ({
|
||||||
|
usePaging: jest.fn().mockReturnValue({
|
||||||
|
currentPage: 1,
|
||||||
|
showPagination: true,
|
||||||
|
pageSize: 10,
|
||||||
|
handlePageChange: jest.fn(),
|
||||||
|
handlePagingChange: jest.fn(),
|
||||||
|
handlePageSizeChange: jest.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('../../utils/EntityUtils', () => ({
|
jest.mock('../../utils/EntityUtils', () => ({
|
||||||
getEntityName: jest
|
getEntityName: jest
|
||||||
.fn()
|
.fn()
|
||||||
|
@ -27,6 +27,7 @@ import { CustomPropertyTable } from '../../components/common/CustomPropertyTable
|
|||||||
import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1';
|
import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1';
|
||||||
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||||
import Loader from '../../components/common/Loader/Loader';
|
import Loader from '../../components/common/Loader/Loader';
|
||||||
|
import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPrevious.interface';
|
||||||
import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels';
|
import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels';
|
||||||
import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component';
|
import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component';
|
||||||
import ContainerChildren from '../../components/Container/ContainerChildren/ContainerChildren';
|
import ContainerChildren from '../../components/Container/ContainerChildren/ContainerChildren';
|
||||||
@ -63,8 +64,10 @@ import { Tag } from '../../generated/entity/classification/tag';
|
|||||||
import { Container } from '../../generated/entity/data/container';
|
import { Container } from '../../generated/entity/data/container';
|
||||||
import { ThreadType } from '../../generated/entity/feed/thread';
|
import { ThreadType } from '../../generated/entity/feed/thread';
|
||||||
import { Include } from '../../generated/type/include';
|
import { Include } from '../../generated/type/include';
|
||||||
|
import { Paging } from '../../generated/type/paging';
|
||||||
import { TagLabel } from '../../generated/type/tagLabel';
|
import { TagLabel } from '../../generated/type/tagLabel';
|
||||||
import LimitWrapper from '../../hoc/LimitWrapper';
|
import LimitWrapper from '../../hoc/LimitWrapper';
|
||||||
|
import { usePaging } from '../../hooks/paging/usePaging';
|
||||||
import { useApplicationStore } from '../../hooks/useApplicationStore';
|
import { useApplicationStore } from '../../hooks/useApplicationStore';
|
||||||
import { useFqn } from '../../hooks/useFqn';
|
import { useFqn } from '../../hooks/useFqn';
|
||||||
import { FeedCounts } from '../../interface/feed.interface';
|
import { FeedCounts } from '../../interface/feed.interface';
|
||||||
@ -72,6 +75,7 @@ import { postThread } from '../../rest/feedsAPI';
|
|||||||
import {
|
import {
|
||||||
addContainerFollower,
|
addContainerFollower,
|
||||||
getContainerByName,
|
getContainerByName,
|
||||||
|
getContainerChildrenByName,
|
||||||
patchContainerDetails,
|
patchContainerDetails,
|
||||||
removeContainerFollower,
|
removeContainerFollower,
|
||||||
restoreContainer,
|
restoreContainer,
|
||||||
@ -121,6 +125,16 @@ const ContainerPage = () => {
|
|||||||
ThreadType.Conversation
|
ThreadType.Conversation
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
paging,
|
||||||
|
pageSize,
|
||||||
|
currentPage,
|
||||||
|
showPagination,
|
||||||
|
handlePagingChange,
|
||||||
|
handlePageChange,
|
||||||
|
handlePageSizeChange,
|
||||||
|
} = usePaging();
|
||||||
|
|
||||||
const fetchContainerDetail = async (containerFQN: string) => {
|
const fetchContainerDetail = async (containerFQN: string) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
@ -161,13 +175,18 @@ const ContainerPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchContainerChildren = async () => {
|
const fetchContainerChildren = async (pagingOffset?: Paging) => {
|
||||||
setIsChildrenLoading(true);
|
setIsChildrenLoading(true);
|
||||||
try {
|
try {
|
||||||
const { children } = await getContainerByName(decodedContainerName, {
|
const { data, paging } = await getContainerChildrenByName(
|
||||||
fields: TabSpecificField.CHILDREN,
|
decodedContainerName,
|
||||||
});
|
{
|
||||||
setContainerChildrenData(children);
|
limit: pageSize,
|
||||||
|
offset: pagingOffset?.offset ?? 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setContainerChildrenData(data);
|
||||||
|
handlePagingChange(paging);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorToast(error as AxiosError);
|
showErrorToast(error as AxiosError);
|
||||||
} finally {
|
} finally {
|
||||||
@ -562,6 +581,13 @@ const ContainerPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChildrenPageChange = ({ currentPage }: PagingHandlerParams) => {
|
||||||
|
handlePageChange(currentPage);
|
||||||
|
fetchContainerChildren({
|
||||||
|
offset: (currentPage - 1) * pageSize,
|
||||||
|
} as Paging);
|
||||||
|
};
|
||||||
|
|
||||||
const handleTagSelection = async (selectedTags: EntityTags[]) => {
|
const handleTagSelection = async (selectedTags: EntityTags[]) => {
|
||||||
const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags);
|
const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags);
|
||||||
|
|
||||||
@ -611,6 +637,14 @@ const ContainerPage = () => {
|
|||||||
childrenList={containerChildrenData}
|
childrenList={containerChildrenData}
|
||||||
fetchChildren={fetchContainerChildren}
|
fetchChildren={fetchContainerChildren}
|
||||||
isLoading={isChildrenLoading}
|
isLoading={isChildrenLoading}
|
||||||
|
pagingHookData={{
|
||||||
|
paging,
|
||||||
|
pageSize,
|
||||||
|
currentPage,
|
||||||
|
showPagination,
|
||||||
|
handleChildrenPageChange,
|
||||||
|
handlePageSizeChange,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ContainerDataModel
|
<ContainerDataModel
|
||||||
@ -684,6 +718,14 @@ const ContainerPage = () => {
|
|||||||
childrenList={containerChildrenData}
|
childrenList={containerChildrenData}
|
||||||
fetchChildren={fetchContainerChildren}
|
fetchChildren={fetchContainerChildren}
|
||||||
isLoading={isChildrenLoading}
|
isLoading={isChildrenLoading}
|
||||||
|
pagingHookData={{
|
||||||
|
paging,
|
||||||
|
pageSize,
|
||||||
|
currentPage,
|
||||||
|
showPagination,
|
||||||
|
handleChildrenPageChange,
|
||||||
|
handlePageSizeChange,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { PagingWithoutTotal, RestoreRequestType } from 'Models';
|
import { PagingResponse, PagingWithoutTotal, RestoreRequestType } from 'Models';
|
||||||
import { QueryVote } from '../components/Database/TableQueries/TableQueries.interface';
|
import { QueryVote } from '../components/Database/TableQueries/TableQueries.interface';
|
||||||
import { APPLICATION_JSON_CONTENT_TYPE_HEADER } from '../constants/constants';
|
import { APPLICATION_JSON_CONTENT_TYPE_HEADER } from '../constants/constants';
|
||||||
import { Container } from '../generated/entity/data/container';
|
import { Container } from '../generated/entity/data/container';
|
||||||
@ -20,7 +20,7 @@ import { EntityHistory } from '../generated/type/entityHistory';
|
|||||||
import { EntityReference } from '../generated/type/entityReference';
|
import { EntityReference } from '../generated/type/entityReference';
|
||||||
import { Include } from '../generated/type/include';
|
import { Include } from '../generated/type/include';
|
||||||
import { Paging } from '../generated/type/paging';
|
import { Paging } from '../generated/type/paging';
|
||||||
import { ListParams } from '../interface/API.interface';
|
import { ListParams, ListParamsWithOffset } from '../interface/API.interface';
|
||||||
import { ServicePageData } from '../pages/ServiceDetailsPage/ServiceDetailsPage';
|
import { ServicePageData } from '../pages/ServiceDetailsPage/ServiceDetailsPage';
|
||||||
import { getEncodedFqn } from '../utils/StringsUtils';
|
import { getEncodedFqn } from '../utils/StringsUtils';
|
||||||
import APIClient from './index';
|
import APIClient from './index';
|
||||||
@ -63,6 +63,20 @@ export const getContainerByName = async (name: string, params?: ListParams) => {
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getContainerChildrenByName = async (
|
||||||
|
name: string,
|
||||||
|
params?: ListParamsWithOffset
|
||||||
|
) => {
|
||||||
|
const response = await APIClient.get<PagingResponse<Container['children']>>(
|
||||||
|
`${BASE_URL}/name/${getEncodedFqn(name)}/children`,
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
export const patchContainerDetails = async (id: string, data: Operation[]) => {
|
export const patchContainerDetails = async (id: string, data: Operation[]) => {
|
||||||
const response = await APIClient.patch<Operation[], AxiosResponse<Container>>(
|
const response = await APIClient.patch<Operation[], AxiosResponse<Container>>(
|
||||||
`${BASE_URL}/${id}`,
|
`${BASE_URL}/${id}`,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user