diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx index 614c77afdee..84bb2ddd8de 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx @@ -54,7 +54,6 @@ import TitleBreadcrumb from '../../components/common/title-breadcrumb/title-brea import { TitleBreadcrumbProps } from '../../components/common/title-breadcrumb/title-breadcrumb.interface'; import PageContainerV1 from '../../components/containers/PageContainerV1'; import Loader from '../../components/Loader/Loader'; -import RequestDescriptionModal from '../../components/Modals/RequestDescriptionModal/RequestDescriptionModal'; import { usePermissionProvider } from '../../components/PermissionProvider/PermissionProvider'; import { OperationPermission, @@ -80,7 +79,6 @@ import { CreateThread } from '../../generated/api/feed/createThread'; import { DatabaseSchema } from '../../generated/entity/data/databaseSchema'; import { Table } from '../../generated/entity/data/table'; import { Post, Thread } from '../../generated/entity/feed/thread'; -import { EntityReference } from '../../generated/entity/teams/user'; import { Paging } from '../../generated/type/paging'; import { useInfiniteScroll } from '../../hooks/useInfiniteScroll'; import jsonData from '../../jsons/en'; @@ -91,10 +89,10 @@ import { import { databaseSchemaDetailsTabs, getCurrentDatabaseSchemaDetailsTab, + getQueryStringForSchemaTables, getTablesFromSearchResponse, } from '../../utils/DatabaseSchemaDetailsUtils'; import { getEntityFeedLink } from '../../utils/EntityUtils'; -import { getDefaultValue } from '../../utils/FeedElementUtils'; import { deletePost, getEntityFieldThreadCounts, @@ -144,7 +142,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { >([]); const [threadLink, setThreadLink] = useState(''); - const [selectedField, setSelectedField] = useState(''); const [paging, setPaging] = useState({} as Paging); const [currentTablesPage, setCurrentTablesPage] = useState(INITIAL_PAGING_VALUE); @@ -230,13 +227,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { setThreadLink(''); }; - const onEntityFieldSelect = (value: string) => { - setSelectedField(value); - }; - const closeRequestModal = () => { - setSelectedField(''); - }; - const getEntityFeedCount = () => { getFeedCount( getEntityFeedLink(EntityType.DATABASE_SCHEMA, databaseSchemaFQN) @@ -330,9 +320,13 @@ const DatabaseSchemaPage: FunctionComponent = () => { try { setCurrentTablesPage(pageNumber); const res = await searchQuery({ + query: getQueryStringForSchemaTables( + databaseSchema.service, + databaseSchema.database, + databaseSchema + ), pageNumber, pageSize: PAGE_SIZE, - queryFilter: { 'databaseSchema.name': databaseSchema.name }, searchIndex: SearchIndex.TABLE, includeDeleted: false, }); @@ -741,7 +735,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { onCancel={onCancel} onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} - onEntityFieldSelect={onEntityFieldSelect} onThreadLinkSelect={onThreadLinkSelect} /> @@ -802,23 +795,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { /> ) : null} - - {selectedField ? ( - - ) : null} - ) : ( diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.test.tsx new file mode 100644 index 00000000000..3e7eb6ace38 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.test.tsx @@ -0,0 +1,325 @@ +/* + * Copyright 2022 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 { act, fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter, useParams } from 'react-router-dom'; +import { getDatabaseSchemaDetailsByFQN } from '../../axiosAPIs/databaseAPI'; +import { usePermissionProvider } from '../../components/PermissionProvider/PermissionProvider'; +import DatabaseSchemaPageComponent from './DatabaseSchemaPage.component'; +import { + mockEntityPermissions, + mockGetAllFeedsData, + mockGetDatabaseSchemaDetailsByFQNData, + mockGetFeedCountData, + mockPatchDatabaseSchemaDetailsData, + mockPostFeedByIdData, + mockPostThreadData, + mockSearchQueryData, +} from './mocks/DatabaseSchemaPage.mock'; + +jest.mock('../../utils/ToastUtils', () => ({ + showErrorToast: jest + .fn() + .mockImplementation(({ children }) =>
{children}
), +})); + +jest.mock('../../components/Loader/Loader', () => + jest.fn().mockImplementation(() =>
Loader
) +); + +jest.mock('../../components/containers/PageContainerV1', () => + jest + .fn() + .mockImplementation(({ children }) => ( +
{children}
+ )) +); + +jest.mock( + '../../components/common/title-breadcrumb/title-breadcrumb.component', + () => + jest + .fn() + .mockImplementation(() => ( +
titleBreadcrumb
+ )) +); + +jest.mock('../../components/common/TabsPane/TabsPane', () => + jest.fn().mockImplementation(() =>
TabsPane
) +); + +jest.mock( + '../../components/common/rich-text-editor/RichTextEditorPreviewer', + () => + jest + .fn() + .mockImplementation(() => ( +
RichTextEditorPreviewer
+ )) +); + +jest.mock('../../components/common/next-previous/NextPrevious', () => + jest + .fn() + .mockImplementation(() => ( +
NextPrevious
+ )) +); + +jest.mock( + '../../components/common/error-with-placeholder/ErrorPlaceHolder', + () => + jest + .fn() + .mockImplementation(({ children }) => ( +
{children}
+ )) +); +jest.mock( + '../../components/common/EntitySummaryDetails/EntitySummaryDetails', + () => + jest + .fn() + .mockImplementation(() => ( +
EntitySummaryDetails
+ )) +); + +jest.mock( + '../../components/common/entityPageInfo/ManageButton/ManageButton', + () => + jest + .fn() + .mockImplementation(() => ( +
ManageButton
+ )) +); + +jest.mock('../../components/common/description/Description', () => + jest + .fn() + .mockImplementation( + ({ onThreadLinkSelect, onDescriptionEdit, onDescriptionUpdate }) => ( +
{ + onThreadLinkSelect('threadLink'); + onDescriptionEdit(); + onDescriptionUpdate('Updated Description'); + }}> + Description +
+ ) + ) +); + +jest.mock( + '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel', + () => + jest + .fn() + .mockImplementation(() => ( +
ActivityThreadPanel
+ )) +); + +jest.mock( + '../../components/ActivityFeed/ActivityFeedList/ActivityFeedList', + () => + jest + .fn() + .mockImplementation(() => ( +
ActivityFeedList
+ )) +); + +jest.mock('../../components/PermissionProvider/PermissionProvider', () => ({ + usePermissionProvider: jest.fn().mockImplementation(() => ({ + getEntityPermissionByFqn: jest + .fn() + .mockImplementation(() => Promise.resolve(mockEntityPermissions)), + })), +})); + +jest.mock('../../axiosAPIs/searchAPI', () => ({ + searchQuery: jest + .fn() + .mockImplementation(() => Promise.resolve(mockSearchQueryData)), +})); + +jest.mock('../../axiosAPIs/feedsAPI', () => ({ + getAllFeeds: jest + .fn() + .mockImplementation(() => Promise.resolve(mockGetAllFeedsData)), + getFeedCount: jest + .fn() + .mockImplementation(() => Promise.resolve(mockGetFeedCountData)), + postFeedById: jest + .fn() + .mockImplementation(() => Promise.resolve(mockPostFeedByIdData)), + postThread: jest + .fn() + .mockImplementation(() => Promise.resolve(mockPostThreadData)), +})); + +jest.mock('../../axiosAPIs/databaseAPI', () => ({ + getDatabaseSchemaDetailsByFQN: jest + .fn() + .mockImplementation(() => + Promise.resolve(mockGetDatabaseSchemaDetailsByFQNData) + ), + patchDatabaseSchemaDetails: jest + .fn() + .mockImplementation(() => + Promise.resolve(mockPatchDatabaseSchemaDetailsData) + ), +})); + +jest.mock('../../AppState', () => ({ + inPageSearchText: '', +})); + +jest.mock('react-router-dom', () => ({ + Link: jest + .fn() + .mockImplementation(({ children }) => ( +
{children}
+ )), + useHistory: jest.fn().mockImplementation(() => ({ + history: { + push: jest.fn(), + }, + })), + useParams: jest.fn().mockImplementation(() => ({ + databaseSchemaFQN: 'sample_data.ecommerce_db.shopify', + tab: 'tables', + })), +})); + +describe('Tests for DatabaseSchemaPage', () => { + it('Page should render properly for "Tables" tab', async () => { + act(() => { + render(, { + wrapper: MemoryRouter, + }); + }); + + const pageContainer = await screen.findByTestId('PageContainer'); + const titleBreadcrumb = await screen.findByTestId('TitleBreadcrumb'); + const tabsPane = await screen.findByTestId('TabsPane'); + const richTextEditorPreviewer = await screen.findAllByTestId( + 'RichTextEditorPreviewer' + ); + const entitySummaryDetails = await screen.findByTestId( + 'EntitySummaryDetails' + ); + const manageButton = await screen.findByTestId('ManageButton'); + const description = await screen.findByTestId('Description'); + const nextPrevious = await screen.findByTestId('NextPrevious'); + const databaseSchemaTable = await screen.findByTestId( + 'databaseSchema-tables' + ); + + expect(pageContainer).toBeInTheDocument(); + expect(titleBreadcrumb).toBeInTheDocument(); + expect(tabsPane).toBeInTheDocument(); + expect(richTextEditorPreviewer).toHaveLength(10); + expect(entitySummaryDetails).toBeInTheDocument(); + expect(manageButton).toBeInTheDocument(); + expect(description).toBeInTheDocument(); + expect(nextPrevious).toBeInTheDocument(); + expect(databaseSchemaTable).toBeInTheDocument(); + }); + + it('Activity Feed List should render properly for "Activity Feeds" tab', async () => { + (useParams as jest.Mock).mockImplementationOnce(() => ({ + databaseSchemaFQN: 'sample_data.ecommerce_db.shopify', + tab: 'activity_feed', + })); + + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + const activityFeedList = await screen.findByTestId('ActivityFeedList'); + + expect(activityFeedList).toBeInTheDocument(); + }); + + it('AcivityThreadPanel should render properly after clicked on thread panel button', async () => { + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + const description = await screen.findByTestId('Description'); + + expect(description).toBeInTheDocument(); + + act(() => { + fireEvent.click(description); + }); + + const activityThreadPanel = await screen.findByTestId( + 'ActivityThreadPanel' + ); + + expect(activityThreadPanel).toBeInTheDocument(); + }); + + it('ErrorPlaceholder should be displayed in case error occurs while fetching database schema details', async () => { + (getDatabaseSchemaDetailsByFQN as jest.Mock).mockImplementationOnce(() => + Promise.reject('An error occurred') + ); + + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + const errorPlaceHolder = await screen.findByTestId('ErrorPlaceHolder'); + const errorMessage = await screen.findByTestId('error-message'); + + expect(errorPlaceHolder).toBeInTheDocument(); + expect(errorMessage).toHaveTextContent('An error occurred'); + }); + + it('ErrorPlaceholder should be shown in case of not viewing permissions', async () => { + (usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({ + getEntityPermissionByFqn: jest.fn().mockImplementation(() => + Promise.resolve({ + ...mockEntityPermissions, + ViewAll: false, + ViewBasic: false, + }) + ), + })); + + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + const errorPlaceHolder = await screen.findByTestId('ErrorPlaceHolder'); + + expect(errorPlaceHolder).toBeInTheDocument(); + expect(errorPlaceHolder).toHaveTextContent('message.no-permission-to-view'); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/mocks/DatabaseSchemaPage.mock.ts b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/mocks/DatabaseSchemaPage.mock.ts new file mode 100644 index 00000000000..7f3415de277 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/mocks/DatabaseSchemaPage.mock.ts @@ -0,0 +1,256 @@ +/* + * Copyright 2022 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. + */ + +export const mockSearchQueryData = { + hits: { + total: { + value: 18, + relation: 'eq', + }, + hits: [ + { + _source: { + id: '0e8ec01e-a57f-4173-8d30-deda453174d0', + name: 'dim.api/client', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify."dim.api/client"', + description: 'Description', + displayName: 'dim.api/client', + }, + }, + { + _source: { + id: '2bcadace-b42b-4f86-b865-72fd1bc0338f', + name: 'dim.product', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify."dim.product"', + description: 'Description', + displayName: 'dim.product', + }, + }, + { + _source: { + id: '3a39a1ef-0ef4-44ca-ad01-abe389b69a7c', + name: 'dim.shop', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify."dim.shop"', + description: + 'This dimension table contains online shop information. This table contains one shop per row.', + displayName: 'dim.shop', + }, + }, + { + _source: { + id: '06d629d7-6d38-4e57-83d2-c9c4235a7f66', + name: 'dim_address', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify.dim_address', + description: 'Description', + displayName: 'dim_address', + }, + }, + { + _source: { + id: 'a3bbcfef-9704-4cc5-b4ab-f5356afb9c79', + name: 'dim_address_clean', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean', + description: 'Created from dim_address after a small cleanup.', + displayName: 'dim_address_clean', + }, + }, + { + _source: { + id: 'a6d06d44-32f0-491a-99e9-5c319dd8ff23', + name: 'dim_customer', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify.dim_customer', + description: 'Description', + displayName: 'dim_customer', + }, + }, + { + _source: { + id: '86076923-2365-454c-897f-36be6ca02ef2', + name: 'dim_location', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify.dim_location', + description: 'Description', + displayName: 'dim_location', + }, + }, + { + _source: { + id: 'eba81b7a-d086-4daf-ab88-041d4703a2e7', + name: 'dim_staff', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify.dim_staff', + description: 'Description', + displayName: 'dim_staff', + }, + }, + { + _source: { + id: 'bc4c7a5f-2964-4582-adb1-2b523d6035b8', + name: 'fact_line_item', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify.fact_line_item', + description: 'Description', + displayName: 'fact_line_item', + }, + }, + { + _source: { + id: '833438ab-a267-4331-9f9e-be1d9399bda6', + name: 'fact_order', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify.fact_order', + description: 'Description', + displayName: 'fact_order', + }, + }, + ], + }, + aggregations: {}, +}; + +export const mockGetFeedCountData = { + totalCount: 0, + counts: [], +}; + +export const mockGetDatabaseSchemaDetailsByFQNData = { + id: '06f0c9ef-708a-47e1-a36e-0a2b864c9e5d', + name: 'shopify', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify', + description: + 'This **mock** database contains schema related to shopify sales and orders with related dimension tables.', + href: 'http://localhost:8585/api/v1/databaseSchemas/06f0c9ef-708a-47e1-a36e-0a2b864c9e5d', + service: { + id: '0e736c54-0c2b-41bc-a877-6df3cdb5a2dd', + type: 'databaseService', + name: 'sample_data', + fullyQualifiedName: 'sample_data', + deleted: false, + href: 'http://localhost:8585/api/v1/services/databaseServices/0e736c54-0c2b-41bc-a877-6df3cdb5a2dd', + }, + serviceType: 'BigQuery', + database: { + id: '2e45b3a0-6cfa-470a-862e-4b89f28965c3', + type: 'database', + name: 'ecommerce_db', + fullyQualifiedName: 'sample_data.ecommerce_db', + description: + 'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.', + deleted: false, + href: 'http://localhost:8585/api/v1/databases/2e45b3a0-6cfa-470a-862e-4b89f28965c3', + }, + deleted: false, +}; + +export const mockGetAllFeedsData = { + data: [], + paging: { + total: 0, + }, +}; + +export const mockPostThreadData = { + id: 'f5d62891-3381-4adc-91dc-0a886b9cd751', + type: 'Conversation', + href: 'http://localhost:8585/api/v1/feed/f5d62891-3381-4adc-91dc-0a886b9cd751', + threadTs: 1670412439695, + about: '<#E::databaseSchema::sample_data.ecommerce_db.shopify::description>', + entityId: '06f0c9ef-708a-47e1-a36e-0a2b864c9e5d', + createdBy: 'admin', + updatedAt: 1670412439696, + updatedBy: 'admin', + resolved: false, + message: 'Testing Conversation', + postsCount: 0, + posts: [], + reactions: [], +}; + +export const mockPostFeedByIdData = { + id: 'f5d62891-3381-4adc-91dc-0a886b9cd751', + type: 'Conversation', + href: 'http://localhost:8585/api/v1/feed/f5d62891-3381-4adc-91dc-0a886b9cd751', + threadTs: 1670412439695, + about: '<#E::databaseSchema::sample_data.ecommerce_db.shopify::description>', + entityId: '06f0c9ef-708a-47e1-a36e-0a2b864c9e5d', + createdBy: 'admin', + updatedAt: 1670414002766, + updatedBy: 'admin', + resolved: false, + message: 'Testing Conversation', + postsCount: 2, + posts: [ + { + id: '943f793a-35be-4942-adfe-6b5457f95978', + message: 'Testing', + postTs: 1670414002762, + from: 'admin', + reactions: [], + }, + ], + reactions: [], +}; +export const mockPatchDatabaseSchemaDetailsData = { + id: '06f0c9ef-708a-47e1-a36e-0a2b864c9e5d', + name: 'shopify', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify', + description: + 'This **mock** database contains schema related to shopify sales and orders with related dimension tables. updated', + version: 0.3, + updatedAt: 1670414195234, + updatedBy: 'admin', + href: 'http://localhost:8585/api/v1/databaseSchemas/06f0c9ef-708a-47e1-a36e-0a2b864c9e5d', + service: { + id: '0e736c54-0c2b-41bc-a877-6df3cdb5a2dd', + type: 'databaseService', + name: 'sample_data', + fullyQualifiedName: 'sample_data', + deleted: false, + href: 'http://localhost:8585/api/v1/services/databaseServices/0e736c54-0c2b-41bc-a877-6df3cdb5a2dd', + }, + serviceType: 'BigQuery', + database: { + id: '2e45b3a0-6cfa-470a-862e-4b89f28965c3', + type: 'database', + name: 'ecommerce_db', + fullyQualifiedName: 'sample_data.ecommerce_db', + description: + 'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.', + deleted: false, + href: 'http://localhost:8585/api/v1/databases/2e45b3a0-6cfa-470a-862e-4b89f28965c3', + }, + changeDescription: { + fieldsAdded: [], + fieldsUpdated: [ + { + name: 'description', + oldValue: + 'This **mock** database contains schema related to shopify sales and orders with related dimension tables. ', + newValue: + 'This **mock** database contains schema related to shopify sales and orders with related dimension tables. updated', + }, + ], + fieldsDeleted: [], + previousVersion: 0.2, + }, + deleted: false, +}; + +export const mockEntityPermissions = { + Create: true, + Delete: true, + ViewAll: true, + ViewBasic: true, + EditAll: true, + EditDescription: true, + EditDisplayName: true, + EditCustomFields: true, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.ts index 58ffa00e61b..0643a428adb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.ts @@ -1,5 +1,7 @@ import { TabSpecificField } from '../enums/entity.enum'; import { SearchIndex } from '../enums/search.enum'; +import { DatabaseSchema } from '../generated/entity/data/databaseSchema'; +import { EntityReference } from '../generated/type/entityReference'; import { SearchResponse, TableSearchSource, @@ -38,3 +40,10 @@ export const getCurrentDatabaseSchemaDetailsTab = (tab: string) => { export const getTablesFromSearchResponse = ( res: SearchResponse ) => res.hits.hits.map((hit) => hit._source); + +export const getQueryStringForSchemaTables = ( + serviceName: EntityReference, + databaseName: EntityReference, + schemaName: DatabaseSchema +) => + `(service.name:${serviceName.name}) AND (database.name:${databaseName.name}) AND (databaseSchema.name:${schemaName.name})`;