mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-12 17:26:43 +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("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(
|
||||
value =
|
||||
"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.STORAGE_SERVICE;
|
||||
import static org.openmetadata.service.Entity.populateEntityFieldTags;
|
||||
import static org.openmetadata.service.util.EntityUtil.getEntityReferences;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
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.FullyQualifiedName;
|
||||
import org.openmetadata.service.util.JsonUtils;
|
||||
import org.openmetadata.service.util.ResultList;
|
||||
|
||||
public class ContainerRepository extends EntityRepository<Container> {
|
||||
private static final String CONTAINER_UPDATE_FIELDS = "dataModel";
|
||||
@ -223,6 +225,49 @@ public class ContainerRepository extends EntityRepository<Container> {
|
||||
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 {
|
||||
private final Column column;
|
||||
|
||||
|
@ -546,4 +546,39 @@ public class ContainerResource extends EntityResource<Container, ContainerReposi
|
||||
@Valid RestoreEntity restore) {
|
||||
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.Map;
|
||||
import java.util.UUID;
|
||||
import javax.ws.rs.client.WebTarget;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -394,6 +395,7 @@ public class ContainerResourceTest extends EntityResourceTest<Container, CreateC
|
||||
.withOwners(List.of(DATA_CONSUMER.getEntityReference()))
|
||||
.withSize(0.0);
|
||||
Container rootContainer = createAndCheckEntity(createRootContainer, ADMIN_AUTH_HEADERS);
|
||||
String rootContainerFQN = rootContainer.getFullyQualifiedName();
|
||||
|
||||
CreateContainer createChildOneContainer =
|
||||
new CreateContainer()
|
||||
@ -485,6 +487,19 @@ public class ContainerResourceTest extends EntityResourceTest<Container, CreateC
|
||||
ResultList<Container> rootContainerList = listEntities(queryParams, ADMIN_AUTH_HEADERS);
|
||||
assertEquals(1, rootContainerList.getData().size());
|
||||
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
|
||||
@ -696,6 +711,14 @@ public class ContainerResourceTest extends EntityResourceTest<Container, CreateC
|
||||
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
|
||||
void testInheritedPermissionFromParent(TestInfo test) throws IOException {
|
||||
// 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 { Operation } from 'fast-json-patch';
|
||||
import { isUndefined } from 'lodash';
|
||||
import { SERVICE_TYPE } from '../../constant/service';
|
||||
import { ServiceTypes } from '../../constant/settings';
|
||||
import { uuid } from '../../utils/common';
|
||||
@ -89,6 +90,7 @@ export class ContainerClass extends EntityClass {
|
||||
entityResponseData: ResponseDataWithServiceType =
|
||||
{} as ResponseDataWithServiceType;
|
||||
childResponseData: ResponseDataType = {} as ResponseDataType;
|
||||
childArrayResponseData: ResponseDataType[] = [];
|
||||
|
||||
constructor(name?: string) {
|
||||
super(EntityTypeEndpoint.Container);
|
||||
@ -98,7 +100,10 @@ export class ContainerClass extends EntityClass {
|
||||
this.serviceCategory = SERVICE_TYPE.Storage;
|
||||
}
|
||||
|
||||
async create(apiContext: APIRequestContext) {
|
||||
async create(
|
||||
apiContext: APIRequestContext,
|
||||
customChildContainer?: { name: string; displayName: string }[]
|
||||
) {
|
||||
const serviceResponse = await apiContext.post(
|
||||
'/api/v1/services/storageServices',
|
||||
{
|
||||
@ -112,19 +117,39 @@ export class ContainerClass extends EntityClass {
|
||||
this.serviceResponseData = await serviceResponse.json();
|
||||
this.entityResponseData = await entityResponse.json();
|
||||
|
||||
const childContainer = {
|
||||
...this.childContainer,
|
||||
parent: {
|
||||
id: this.entityResponseData.id,
|
||||
type: 'container',
|
||||
},
|
||||
};
|
||||
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,
|
||||
});
|
||||
|
||||
const childResponse = await apiContext.post('/api/v1/containers', {
|
||||
data: childContainer,
|
||||
});
|
||||
childArrayResponseData.push(await childResponse.json());
|
||||
}
|
||||
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 {
|
||||
service: serviceResponse.body,
|
||||
|
@ -10,12 +10,26 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { pagingObject } from '../../../constants/constants';
|
||||
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 mockHandleChildrenPageChange = jest.fn();
|
||||
const mockHandlePageSizeChange = jest.fn();
|
||||
|
||||
const mockChildrenList = [
|
||||
{
|
||||
id: '1',
|
||||
@ -36,6 +50,14 @@ const mockChildrenList = [
|
||||
const mockDataProps = {
|
||||
childrenList: mockChildrenList,
|
||||
fetchChildren: mockFetchChildren,
|
||||
pagingHookData: {
|
||||
paging: pagingObject,
|
||||
pageSize: 15,
|
||||
currentPage: 1,
|
||||
showPagination: true,
|
||||
handleChildrenPageChange: mockHandleChildrenPageChange,
|
||||
handlePageSizeChange: mockHandlePageSizeChange,
|
||||
},
|
||||
};
|
||||
|
||||
describe('ContainerChildren', () => {
|
||||
@ -70,6 +92,22 @@ describe('ContainerChildren', () => {
|
||||
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', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
@ -108,4 +146,44 @@ describe('ContainerChildren', () => {
|
||||
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
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Typography } from 'antd';
|
||||
import { Col, Row, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import React, { FC, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -19,23 +19,43 @@ import { getEntityDetailsPath } from '../../../constants/constants';
|
||||
import { EntityType } from '../../../enums/entity.enum';
|
||||
import { Container } from '../../../generated/entity/data/container';
|
||||
import { EntityReference } from '../../../generated/type/entityReference';
|
||||
import { Paging } from '../../../generated/type/paging';
|
||||
import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils';
|
||||
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 Table from '../../common/Table/Table';
|
||||
|
||||
interface ContainerChildrenProps {
|
||||
childrenList: Container['children'];
|
||||
isLoading?: boolean;
|
||||
pagingHookData: {
|
||||
paging: Paging;
|
||||
pageSize: number;
|
||||
currentPage: number;
|
||||
showPagination: boolean;
|
||||
handleChildrenPageChange: (data: PagingHandlerParams) => void;
|
||||
handlePageSizeChange: (page: number) => void;
|
||||
};
|
||||
fetchChildren: () => void;
|
||||
}
|
||||
|
||||
const ContainerChildren: FC<ContainerChildrenProps> = ({
|
||||
childrenList,
|
||||
isLoading,
|
||||
pagingHookData,
|
||||
fetchChildren,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
paging,
|
||||
pageSize,
|
||||
currentPage,
|
||||
showPagination,
|
||||
handleChildrenPageChange,
|
||||
handlePageSizeChange,
|
||||
} = pagingHookData;
|
||||
|
||||
const columns: ColumnsType<EntityReference> = useMemo(
|
||||
() => [
|
||||
@ -83,22 +103,39 @@ const ContainerChildren: FC<ContainerChildrenProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
fetchChildren();
|
||||
}, []);
|
||||
}, [pageSize]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
bordered
|
||||
columns={columns}
|
||||
data-testid="container-list-table"
|
||||
dataSource={childrenList}
|
||||
loading={isLoading}
|
||||
locale={{
|
||||
emptyText: <ErrorPlaceHolder className="p-y-md" />,
|
||||
}}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
/>
|
||||
<Row className="m-b-md" gutter={[0, 16]}>
|
||||
<Col span={24}>
|
||||
<Table
|
||||
bordered
|
||||
columns={columns}
|
||||
data-testid="container-list-table"
|
||||
dataSource={childrenList}
|
||||
loading={isLoading}
|
||||
locale={{
|
||||
emptyText: <ErrorPlaceHolder className="p-y-md" />,
|
||||
}}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -19,3 +19,7 @@ export type ListParams = {
|
||||
after?: string;
|
||||
include?: Include;
|
||||
};
|
||||
|
||||
export type ListParamsWithOffset = ListParams & {
|
||||
offset?: number;
|
||||
};
|
||||
|
@ -184,6 +184,17 @@ jest.mock('../../utils/CommonUtils', () => ({
|
||||
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', () => ({
|
||||
getEntityName: jest
|
||||
.fn()
|
||||
|
@ -27,6 +27,7 @@ import { CustomPropertyTable } from '../../components/common/CustomPropertyTable
|
||||
import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1';
|
||||
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import Loader from '../../components/common/Loader/Loader';
|
||||
import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPrevious.interface';
|
||||
import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels';
|
||||
import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component';
|
||||
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 { ThreadType } from '../../generated/entity/feed/thread';
|
||||
import { Include } from '../../generated/type/include';
|
||||
import { Paging } from '../../generated/type/paging';
|
||||
import { TagLabel } from '../../generated/type/tagLabel';
|
||||
import LimitWrapper from '../../hoc/LimitWrapper';
|
||||
import { usePaging } from '../../hooks/paging/usePaging';
|
||||
import { useApplicationStore } from '../../hooks/useApplicationStore';
|
||||
import { useFqn } from '../../hooks/useFqn';
|
||||
import { FeedCounts } from '../../interface/feed.interface';
|
||||
@ -72,6 +75,7 @@ import { postThread } from '../../rest/feedsAPI';
|
||||
import {
|
||||
addContainerFollower,
|
||||
getContainerByName,
|
||||
getContainerChildrenByName,
|
||||
patchContainerDetails,
|
||||
removeContainerFollower,
|
||||
restoreContainer,
|
||||
@ -121,6 +125,16 @@ const ContainerPage = () => {
|
||||
ThreadType.Conversation
|
||||
);
|
||||
|
||||
const {
|
||||
paging,
|
||||
pageSize,
|
||||
currentPage,
|
||||
showPagination,
|
||||
handlePagingChange,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
} = usePaging();
|
||||
|
||||
const fetchContainerDetail = async (containerFQN: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@ -161,13 +175,18 @@ const ContainerPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchContainerChildren = async () => {
|
||||
const fetchContainerChildren = async (pagingOffset?: Paging) => {
|
||||
setIsChildrenLoading(true);
|
||||
try {
|
||||
const { children } = await getContainerByName(decodedContainerName, {
|
||||
fields: TabSpecificField.CHILDREN,
|
||||
});
|
||||
setContainerChildrenData(children);
|
||||
const { data, paging } = await getContainerChildrenByName(
|
||||
decodedContainerName,
|
||||
{
|
||||
limit: pageSize,
|
||||
offset: pagingOffset?.offset ?? 0,
|
||||
}
|
||||
);
|
||||
setContainerChildrenData(data);
|
||||
handlePagingChange(paging);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
} 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 updatedTags: TagLabel[] | undefined = createTagObject(selectedTags);
|
||||
|
||||
@ -611,6 +637,14 @@ const ContainerPage = () => {
|
||||
childrenList={containerChildrenData}
|
||||
fetchChildren={fetchContainerChildren}
|
||||
isLoading={isChildrenLoading}
|
||||
pagingHookData={{
|
||||
paging,
|
||||
pageSize,
|
||||
currentPage,
|
||||
showPagination,
|
||||
handleChildrenPageChange,
|
||||
handlePageSizeChange,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ContainerDataModel
|
||||
@ -684,6 +718,14 @@ const ContainerPage = () => {
|
||||
childrenList={containerChildrenData}
|
||||
fetchChildren={fetchContainerChildren}
|
||||
isLoading={isChildrenLoading}
|
||||
pagingHookData={{
|
||||
paging,
|
||||
pageSize,
|
||||
currentPage,
|
||||
showPagination,
|
||||
handleChildrenPageChange,
|
||||
handlePageSizeChange,
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -12,7 +12,7 @@
|
||||
*/
|
||||
import { AxiosResponse } from 'axios';
|
||||
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 { APPLICATION_JSON_CONTENT_TYPE_HEADER } from '../constants/constants';
|
||||
import { Container } from '../generated/entity/data/container';
|
||||
@ -20,7 +20,7 @@ import { EntityHistory } from '../generated/type/entityHistory';
|
||||
import { EntityReference } from '../generated/type/entityReference';
|
||||
import { Include } from '../generated/type/include';
|
||||
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 { getEncodedFqn } from '../utils/StringsUtils';
|
||||
import APIClient from './index';
|
||||
@ -63,6 +63,20 @@ export const getContainerByName = async (name: string, params?: ListParams) => {
|
||||
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[]) => {
|
||||
const response = await APIClient.patch<Operation[], AxiosResponse<Container>>(
|
||||
`${BASE_URL}/${id}`,
|
||||
|
Loading…
x
Reference in New Issue
Block a user