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:
sonika-shah 2025-02-20 16:43:30 +05:30 committed by GitHub
parent 88f615ae47
commit aefc36b596
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 489 additions and 35 deletions

View File

@ -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 "

View File

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

View File

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

View File

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

View File

@ -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}`,
};
});

View File

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

View File

@ -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,19 +117,39 @@ 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();
const childContainer = { if (!isUndefined(customChildContainer)) {
...this.childContainer, const childArrayResponseData: ResponseDataType[] = [];
parent: { for (const child of customChildContainer) {
id: this.entityResponseData.id, const childContainer = {
type: 'container', ...child,
}, service: this.service.name,
}; parent: {
id: this.entityResponseData.id,
type: 'container',
},
};
const childResponse = await apiContext.post('/api/v1/containers', {
data: childContainer,
});
const childResponse = await apiContext.post('/api/v1/containers', { childArrayResponseData.push(await childResponse.json());
data: childContainer, }
}); this.childArrayResponseData = childArrayResponseData;
} else {
const childContainer = {
...this.childContainer,
parent: {
id: this.entityResponseData.id,
type: 'container',
},
};
this.childResponseData = await childResponse.json(); const childResponse = await apiContext.post('/api/v1/containers', {
data: childContainer,
});
this.childResponseData = await childResponse.json();
}
return { return {
service: serviceResponse.body, service: serviceResponse.body,

View File

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

View File

@ -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,22 +103,39 @@ const ContainerChildren: FC<ContainerChildrenProps> = ({
useEffect(() => { useEffect(() => {
fetchChildren(); fetchChildren();
}, []); }, [pageSize]);
return ( return (
<Table <Row className="m-b-md" gutter={[0, 16]}>
bordered <Col span={24}>
columns={columns} <Table
data-testid="container-list-table" bordered
dataSource={childrenList} columns={columns}
loading={isLoading} data-testid="container-list-table"
locale={{ dataSource={childrenList}
emptyText: <ErrorPlaceHolder className="p-y-md" />, loading={isLoading}
}} locale={{
pagination={false} emptyText: <ErrorPlaceHolder className="p-y-md" />,
rowKey="id" }}
size="small" pagination={false}
/> rowKey="id"
size="small"
/>
</Col>
<Col span={24}>
{showPagination && (
<NextPrevious
isNumberBased
currentPage={currentPage}
isLoading={isLoading}
pageSize={pageSize}
paging={paging}
pagingHandler={handleChildrenPageChange}
onShowSizeChange={handlePageSizeChange}
/>
)}
</Col>
</Row>
); );
}; };

View File

@ -19,3 +19,7 @@ export type ListParams = {
after?: string; after?: string;
include?: Include; include?: Include;
}; };
export type ListParamsWithOffset = ListParams & {
offset?: number;
};

View File

@ -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()

View File

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

View File

@ -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}`,