fix #21572 : Schema / tags/ api collection switch does not update content in UI (#21583)

* fix schema update issue

* added test

* fix issue for tags and api page

* added test for tags page

* update test for api collection

* remove i18 mock

* address pr comments

(cherry picked from commit 852fa432c587cc4646acdcfa3c83241155b78569)
This commit is contained in:
Shrushti Polekar 2025-06-06 15:02:29 +05:30 committed by OpenMetadata Release Bot
parent 745ff15e91
commit fd37cb9c34
6 changed files with 481 additions and 9 deletions

View File

@ -0,0 +1,218 @@
/*
* Copyright 2024 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 { render, waitFor } from '@testing-library/react';
import React from 'react';
import { useParams } from 'react-router-dom';
import { EntityType, TabSpecificField } from '../../enums/entity.enum';
import { Include } from '../../generated/type/include';
import { useFqn } from '../../hooks/useFqn';
import { getApiCollectionByFQN } from '../../rest/apiCollectionsAPI';
import { getApiEndPoints } from '../../rest/apiEndpointsAPI';
import { getFeedCounts } from '../../utils/CommonUtils';
import APICollectionPage from './APICollectionPage';
jest.mock('../../rest/apiCollectionsAPI', () => ({
getApiCollectionByFQN: jest.fn().mockResolvedValue({}),
restoreApiCollection: jest.fn().mockResolvedValue({ version: 1 }),
patchApiCollection: jest.fn().mockResolvedValue({}),
updateApiCollectionVote: jest.fn().mockResolvedValue({}),
}));
jest.mock('../../rest/apiEndpointsAPI', () => ({
getApiEndPoints: jest.fn().mockResolvedValue({ paging: { total: 0 } }),
}));
jest.mock('../../utils/CommonUtils', () => ({
getFeedCounts: jest.fn(),
getEntityMissingError: jest.fn(),
showErrorToast: jest.fn(),
showSuccessToast: jest.fn(),
getCountBadge: jest.fn().mockImplementation((count) => <span>{count}</span>),
}));
jest.mock('../../hooks/useFqn', () => ({
useFqn: jest.fn().mockReturnValue({ fqn: 'api.collection.v1' }),
}));
jest.mock('../../hooks/useCustomPages', () => ({
useCustomPages: jest.fn().mockReturnValue({
customizedPage: null,
isLoading: false,
}),
}));
jest.mock('../../hooks/useTableFilters', () => ({
useTableFilters: jest.fn().mockReturnValue({
filters: { showDeletedEndpoints: false },
setFilters: jest.fn(),
}),
}));
jest.mock('../../context/PermissionProvider/PermissionProvider', () => ({
usePermissionProvider: jest.fn().mockReturnValue({
getEntityPermissionByFqn: jest.fn().mockResolvedValue({
ViewAll: true,
EditAll: true,
}),
}),
}));
jest.mock('react-router-dom', () => ({
useHistory: jest.fn().mockReturnValue({ push: jest.fn() }),
useParams: jest
.fn()
.mockReturnValue({ fqn: 'api.collection.v1', tab: 'api_endpoint' }),
useLocation: jest.fn().mockReturnValue({ pathname: '/test' }),
}));
jest.mock('../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder', () =>
jest.fn().mockImplementation(() => <div>ErrorPlaceHolder</div>)
);
jest.mock('../../components/common/Loader/Loader', () =>
jest.fn().mockImplementation(() => <div>Loader</div>)
);
jest.mock('../../components/AppRouter/withActivityFeed', () => ({
withActivityFeed: jest.fn().mockImplementation((Component) => Component),
}));
jest.mock('../../components/common/DocumentTitle/DocumentTitle', () =>
jest.fn().mockImplementation(() => <div>DocumentTitle</div>)
);
jest.mock(
'../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component',
() => ({
DataAssetsHeader: jest
.fn()
.mockImplementation(() => <div>DataAssetsHeader</div>),
})
);
jest.mock(
'../../components/Customization/GenericProvider/GenericProvider',
() => ({
GenericProvider: jest
.fn()
.mockImplementation(({ children }) => <div>{children}</div>),
})
);
jest.mock('../../utils/AdvancedSearchClassBase', () => {
const mockAutocomplete = () => async () => ({
data: [],
paging: { total: 0 },
});
const AdvancedSearchClassBase = Object.assign(
jest.fn().mockImplementation(() => ({
baseConfig: {
types: {
multiselect: {
widgets: {},
},
select: {
widgets: {
text: {
operators: ['like', 'not_like', 'regexp'],
},
},
},
},
},
})),
{
autocomplete: mockAutocomplete,
}
);
return {
AdvancedSearchClassBase,
__esModule: true,
default: AdvancedSearchClassBase,
};
});
describe('APICollectionPage', () => {
const renderComponent = () => {
return render(<APICollectionPage />);
};
it('should call APIs with updated FQN when FQN changes', async () => {
// Set initial FQN
(useParams as jest.Mock).mockReturnValue({
fqn: 'api.collection.v1',
tab: 'api_endpoint',
});
const { rerender } = renderComponent();
// Verify initial API calls
await waitFor(() => {
expect(getApiCollectionByFQN).toHaveBeenCalledWith('api.collection.v1', {
fields: `${TabSpecificField.OWNERS},${TabSpecificField.TAGS},${TabSpecificField.DOMAIN},${TabSpecificField.VOTES},${TabSpecificField.EXTENSION},${TabSpecificField.DATA_PRODUCTS}`,
include: Include.All,
});
expect(getApiEndPoints).toHaveBeenCalledWith({
apiCollection: 'api.collection.v1',
service: '',
paging: { limit: 0 },
include: Include.NonDeleted,
});
expect(getFeedCounts).toHaveBeenCalledWith(
EntityType.API_COLLECTION,
'api.collection.v1',
expect.any(Function)
);
});
// Clear mocks to track new calls
jest.clearAllMocks();
// Change FQN
(useParams as jest.Mock).mockReturnValue({
fqn: 'api.collection.v2',
tab: 'api_endpoint',
});
(useFqn as jest.Mock).mockReturnValue({ fqn: 'api.collection.v2' });
// Rerender with new FQN
rerender(<APICollectionPage />);
// Verify APIs are called with new FQN
await waitFor(() => {
expect(getApiCollectionByFQN).toHaveBeenCalledWith('api.collection.v2', {
fields: `${TabSpecificField.OWNERS},${TabSpecificField.TAGS},${TabSpecificField.DOMAIN},${TabSpecificField.VOTES},${TabSpecificField.EXTENSION},${TabSpecificField.DATA_PRODUCTS}`,
include: Include.All,
});
expect(getApiEndPoints).toHaveBeenCalledWith({
apiCollection: 'api.collection.v2',
service: '',
paging: { limit: 0 },
include: Include.NonDeleted,
});
expect(getFeedCounts).toHaveBeenCalledWith(
EntityType.API_COLLECTION,
'api.collection.v2',
expect.any(Function)
);
});
// Verify each API was called exactly once with new FQN
expect(getApiCollectionByFQN).toHaveBeenCalledTimes(1);
expect(getApiEndPoints).toHaveBeenCalledTimes(1);
expect(getFeedCounts).toHaveBeenCalledTimes(1);
});
});

View File

@ -144,13 +144,13 @@ const APICollectionPage: FunctionComponent = () => {
setFeedCount(data);
}, []);
const getEntityFeedCount = () => {
const getEntityFeedCount = useCallback(() => {
getFeedCounts(
EntityType.API_COLLECTION,
decodedAPICollectionFQN,
handleFeedCount
);
};
}, [handleFeedCount, decodedAPICollectionFQN]);
const fetchAPICollectionDetails = useCallback(async () => {
try {
@ -350,7 +350,11 @@ const APICollectionPage: FunctionComponent = () => {
fetchAPICollectionDetails();
getEntityFeedCount();
}
}, [viewAPICollectionPermission]);
}, [
viewAPICollectionPermission,
fetchAPICollectionDetails,
getEntityFeedCount,
]);
useEffect(() => {
if (viewAPICollectionPermission && decodedAPICollectionFQN) {

View File

@ -171,13 +171,13 @@ const DatabaseSchemaPage: FunctionComponent = () => {
setFeedCount(data);
}, []);
const getEntityFeedCount = () => {
const getEntityFeedCount = useCallback(() => {
getFeedCounts(
EntityType.DATABASE_SCHEMA,
decodedDatabaseSchemaFQN,
handleFeedCount
);
};
}, [decodedDatabaseSchemaFQN, handleFeedCount]);
const fetchDatabaseSchemaDetails = useCallback(async () => {
try {
@ -403,10 +403,14 @@ const DatabaseSchemaPage: FunctionComponent = () => {
if (viewDatabaseSchemaPermission) {
fetchDatabaseSchemaDetails();
fetchStoreProcedureCount();
getEntityFeedCount();
}
}, [viewDatabaseSchemaPermission]);
}, [
viewDatabaseSchemaPermission,
fetchDatabaseSchemaDetails,
fetchStoreProcedureCount,
getEntityFeedCount,
]);
useEffect(() => {
fetchTableCount();

View File

@ -11,12 +11,13 @@
* limitations under the License.
*/
import { act, render, screen } from '@testing-library/react';
import { act, render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { FEED_COUNT_INITIAL_DATA } from '../../constants/entity.constants';
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
import { getDatabaseSchemaDetailsByFQN } from '../../rest/databaseAPI';
import { getStoredProceduresList } from '../../rest/storedProceduresAPI';
import { getFeedCounts } from '../../utils/CommonUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import DatabaseSchemaPageComponent from './DatabaseSchemaPage.component';
import {
@ -322,4 +323,64 @@ describe('Tests for DatabaseSchemaPage', () => {
expect(await screen.findByText('testSchemaTablesTab')).toBeInTheDocument();
});
it('should refetch data when decodedDatabaseSchemaFQN changes', async () => {
const mockUseParams = jest.requireMock('react-router-dom').useParams;
mockUseParams.mockReturnValue({
fqn: 'sample_data.ecommerce_db.shopify',
tab: 'table',
});
(usePermissionProvider as jest.Mock).mockImplementation(() => ({
getEntityPermissionByFqn: jest.fn().mockResolvedValue({
ViewBasic: true,
}),
}));
const { rerender } = render(<DatabaseSchemaPageComponent />);
// Wait for initial API calls
await waitFor(() => {
expect(getDatabaseSchemaDetailsByFQN).toHaveBeenCalledWith(
'sample_data.ecommerce_db.shopify',
expect.any(Object)
);
expect(getStoredProceduresList).toHaveBeenCalledWith({
databaseSchema: 'sample_data.ecommerce_db.shopify',
limit: 0,
});
expect(getFeedCounts).toHaveBeenCalledWith(
'databaseSchema',
'sample_data.ecommerce_db.shopify',
expect.any(Function)
);
});
jest.clearAllMocks();
mockUseParams.mockReturnValue({
fqn: 'Glue.default.information_schema',
tab: 'table',
});
// Rerender with new FQN
rerender(<DatabaseSchemaPageComponent />);
// API calls should be made again with new FQN
await waitFor(() => {
expect(getDatabaseSchemaDetailsByFQN).toHaveBeenCalledWith(
'Glue.default.information_schema',
expect.any(Object)
);
expect(getStoredProceduresList).toHaveBeenCalledWith({
databaseSchema: 'Glue.default.information_schema',
limit: 0,
});
expect(getFeedCounts).toHaveBeenCalledWith(
'databaseSchema',
'Glue.default.information_schema',
expect.any(Function)
);
});
});
});

View File

@ -0,0 +1,185 @@
/*
* Copyright 2024 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 { render, waitFor } from '@testing-library/react';
import React from 'react';
import { useFqn } from '../../hooks/useFqn';
import { searchData } from '../../rest/miscAPI';
import { getTagByFqn } from '../../rest/tagAPI';
import TagPage from './TagPage';
jest.mock('../../rest/tagAPI', () => ({
getTagByFqn: jest.fn().mockResolvedValue({
name: 'NonSensitive',
fullyQualifiedName: 'PII.NonSensitive',
}),
}));
jest.mock('../../rest/miscAPI', () => ({
searchData: jest.fn().mockResolvedValue({
data: {
hits: {
total: { value: 0 },
},
},
}),
}));
jest.mock('../../hooks/useFqn', () => ({
useFqn: jest.fn(),
}));
jest.mock('react-router-dom', () => ({
useHistory: jest.fn().mockReturnValue({ push: jest.fn() }),
useParams: jest.fn().mockReturnValue({ fqn: 'PII.NonSensitive' }),
useLocation: jest
.fn()
.mockReturnValue({ pathname: '/tags/PII.NonSensitive' }),
}));
jest.mock('../../context/PermissionProvider/PermissionProvider', () => ({
usePermissionProvider: jest.fn().mockReturnValue({
getEntityPermission: jest.fn().mockResolvedValue({
Create: true,
Delete: true,
ViewAll: true,
EditAll: true,
EditDescription: true,
EditDisplayName: true,
EditCustomFields: true,
}),
}),
}));
jest.mock(
'../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider',
() => ({
useActivityFeedProvider: jest.fn().mockReturnValue({
postFeed: jest.fn(),
deleteFeed: jest.fn(),
updateFeed: jest.fn(),
}),
__esModule: true,
default: 'ActivityFeedProvider',
})
);
jest.mock(
'../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component',
() => ({
ActivityFeedTab: jest.fn().mockImplementation(() => <>ActivityFeedTab</>),
})
);
jest.mock('../../components/PageLayoutV1/PageLayoutV1', () => {
return jest.fn().mockImplementation(({ children }) => <div>{children}</div>);
});
jest.mock(
'../../components/common/TitleBreadcrumb/TitleBreadcrumb.component',
() => {
return jest.fn().mockImplementation(() => <div>TitleBreadcrumb</div>);
}
);
jest.mock('../../components/common/EntityDescription/DescriptionV1', () => {
return jest.fn().mockImplementation(() => <div>DescriptionV1</div>);
});
jest.mock('../../components/common/DomainLabel/DomainLabel.component', () => ({
DomainLabel: jest.fn().mockImplementation(() => <div>DomainLabel</div>),
}));
jest.mock('../../components/common/ResizablePanels/ResizablePanels', () => {
return jest.fn().mockImplementation(({ children }) => <div>{children}</div>);
});
jest.mock(
'../../components/Entity/EntityHeader/EntityHeader.component',
() => ({
EntityHeader: jest.fn().mockImplementation(() => <div>EntityHeader</div>),
})
);
jest.mock(
'../../components/Explore/EntitySummaryPanel/EntitySummaryPanel.component',
() => {
return jest.fn().mockImplementation(() => <div>EntitySummaryPanel</div>);
}
);
jest.mock(
'../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.component',
() => {
return jest.fn().mockImplementation(() => <div>AssetsTabs</div>);
}
);
jest.mock('../../components/Modals/EntityDeleteModal/EntityDeleteModal', () => {
return jest.fn().mockImplementation(() => <div>EntityDeleteModal</div>);
});
jest.mock(
'../../components/Modals/EntityNameModal/EntityNameModal.component',
() => {
return jest.fn().mockImplementation(() => <div>EntityNameModal</div>);
}
);
jest.mock('../../components/Modals/StyleModal/StyleModal.component', () => {
return jest.fn().mockImplementation(() => <div>StyleModal</div>);
});
jest.mock(
'../../components/DataAssets/AssetsSelectionModal/AssetSelectionModal',
() => ({
AssetSelectionModal: jest
.fn()
.mockImplementation(() => <div>AssetSelectionModal</div>),
})
);
describe('TagPage', () => {
it('should call getTagData and fetchClassificationTagAssets when tagFqn changes', async () => {
(useFqn as jest.Mock).mockReturnValue({ fqn: 'PII.NonSensitive' });
const { rerender } = render(<TagPage />);
// Verify initial API calls
await waitFor(() => {
expect(getTagByFqn).toHaveBeenCalledWith('PII.NonSensitive', {
fields: 'domain',
});
expect(searchData).toHaveBeenCalled();
});
jest.clearAllMocks();
// Change FQN
(useFqn as jest.Mock).mockReturnValue({ fqn: 'Certification.Gold' });
(getTagByFqn as jest.Mock).mockResolvedValueOnce({
name: 'Gold',
fullyQualifiedName: 'Certification.Gold',
});
rerender(<TagPage />);
await waitFor(() => {
expect(getTagByFqn).toHaveBeenCalledWith('Certification.Gold', {
fields: 'domain',
});
expect(searchData).toHaveBeenCalled();
});
});
});

View File

@ -566,7 +566,7 @@ const TagPage = () => {
useEffect(() => {
getTagData();
fetchClassificationTagAssets();
}, []);
}, [tagFqn]);
useEffect(() => {
if (tagItem) {