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 097e77af120..5787c3ae929 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
@@ -51,14 +51,16 @@ import React, {
} from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom';
-import { ListDataModelParams } from 'rest/dashboardAPI';
import {
getDatabaseSchemaDetailsByFQN,
patchDatabaseSchemaDetails,
restoreDatabaseSchema,
} from 'rest/databaseAPI';
import { getFeedCount, postThread } from 'rest/feedsAPI';
-import { getStoredProceduresList } from 'rest/storedProceduresAPI';
+import {
+ getStoredProceduresList,
+ ListStoredProcedureParams,
+} from 'rest/storedProceduresAPI';
import { getTableList, TableListParams } from 'rest/tableAPI';
import { getEntityMissingError } from 'utils/CommonUtils';
import { getDatabaseSchemaVersionPath } from 'utils/RouterUtils';
@@ -218,7 +220,7 @@ const DatabaseSchemaPage: FunctionComponent = () => {
}, [databaseSchemaFQN]);
const fetchStoreProcedureDetails = useCallback(
- async (params?: ListDataModelParams) => {
+ async (params?: ListStoredProcedureParams) => {
try {
setStoredProcedure((prev) => ({ ...prev, isLoading: true }));
const { data, paging } = await getStoredProceduresList({
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
index e0c9ea270c7..16938a6cdac 100644
--- 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
@@ -11,23 +11,126 @@
* limitations under the License.
*/
-import { act, fireEvent, render, screen } from '@testing-library/react';
+import { act, render, screen } from '@testing-library/react';
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
import React from 'react';
-import { MemoryRouter } from 'react-router-dom';
import { getDatabaseSchemaDetailsByFQN } from 'rest/databaseAPI';
+import { getStoredProceduresList } from 'rest/storedProceduresAPI';
+import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils';
import DatabaseSchemaPageComponent from './DatabaseSchemaPage.component';
import {
- mockEntityPermissions,
- mockGetAllFeedsData,
mockGetDatabaseSchemaDetailsByFQNData,
mockGetFeedCountData,
mockPatchDatabaseSchemaDetailsData,
- mockPostFeedByIdData,
mockPostThreadData,
- mockSearchQueryData,
} from './mocks/DatabaseSchemaPage.mock';
+const mockEntityPermissionByFqn = jest
+ .fn()
+ .mockImplementation(() => DEFAULT_ENTITY_PERMISSION);
+
+jest.mock('components/PermissionProvider/PermissionProvider', () => ({
+ usePermissionProvider: jest.fn().mockImplementation(() => ({
+ getEntityPermissionByFqn: mockEntityPermissionByFqn,
+ })),
+}));
+
+jest.mock(
+ 'components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider',
+ () => ({
+ useActivityFeedProvider: jest.fn().mockImplementation(() => ({
+ postFeed: jest.fn(),
+ deleteFeed: jest.fn(),
+ updateFeed: jest.fn(),
+ })),
+ __esModule: true,
+ default: 'ActivityFeedProvider',
+ })
+);
+
+jest.mock(
+ 'components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component',
+ () => ({
+ ActivityFeedTab: jest
+ .fn()
+ .mockImplementation(() => <>testActivityFeedTab>),
+ })
+);
+
+jest.mock(
+ 'components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel',
+ () => {
+ return jest.fn().mockImplementation(() =>
testActivityThreadPanel
);
+ }
+);
+
+jest.mock(
+ 'components/DataAssets/DataAssetsHeader/DataAssetsHeader.component',
+ () => ({
+ DataAssetsHeader: jest
+ .fn()
+ .mockImplementation(() => testDataAssetsHeader
),
+ })
+);
+
+jest.mock('components/TabsLabel/TabsLabel.component', () =>
+ jest.fn().mockImplementation(({ name }) => {name}
)
+);
+
+jest.mock('components/Tag/TagsContainerV2/TagsContainerV2', () => {
+ return jest.fn().mockImplementation(() => testTagsContainerV2
);
+});
+
+jest.mock('./SchemaTablesTab', () => {
+ return jest.fn().mockReturnValue(testSchemaTablesTab
);
+});
+
+jest.mock('pages/StoredProcedure/StoredProcedureTab', () => {
+ return jest.fn().mockImplementation(() => testStoredProcedureTab
);
+});
+
+jest.mock('components/containers/PageLayoutV1', () => {
+ return jest.fn().mockImplementation(({ children }) => {children}
);
+});
+
+jest.mock('utils/StringsUtils', () => ({
+ getDecodedFqn: jest.fn().mockImplementation((fqn) => fqn),
+}));
+
+jest.mock('rest/storedProceduresAPI', () => ({
+ getStoredProceduresList: jest
+ .fn()
+ .mockImplementation(() =>
+ Promise.resolve({ data: [], paging: { total: 2 } })
+ ),
+}));
+
+jest.mock('rest/tableAPI', () => ({
+ getTableList: jest
+ .fn()
+ .mockImplementation(() =>
+ Promise.resolve({ data: [], paging: { total: 0 } })
+ ),
+}));
+
+jest.mock('utils/CommonUtils', () => ({
+ getEntityMissingError: jest.fn().mockImplementation((error) => error),
+}));
+
+jest.mock('utils/RouterUtils', () => ({
+ getDatabaseSchemaVersionPath: jest.fn().mockImplementation((path) => path),
+}));
+
+jest.mock('../../utils/EntityUtils', () => ({
+ getEntityFeedLink: jest.fn(),
+ getEntityName: jest.fn().mockImplementation((obj) => obj.name),
+}));
+
+jest.mock('../../utils/TableUtils', () => ({
+ getTierTags: jest.fn(),
+ getTagsWithoutTier: jest.fn(),
+}));
+
jest.mock('../../utils/ToastUtils', () => ({
showErrorToast: jest
.fn()
@@ -35,79 +138,23 @@ jest.mock('../../utils/ToastUtils', () => ({
}));
jest.mock('components/Loader/Loader', () =>
- jest.fn().mockImplementation(() => Loader
)
+ jest.fn().mockImplementation(() => testLoader
)
);
-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/FeedEditor/FeedEditor', () => {
- return jest.fn().mockReturnValue(ActivityFeedEditor
);
-});
-
jest.mock('components/common/error-with-placeholder/ErrorPlaceHolder', () =>
- jest
- .fn()
- .mockImplementation(({ children }) => (
- {children}
- ))
-);
-
-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.fn().mockImplementation(() => ErrorPlaceHolder
)
);
jest.mock('components/PermissionProvider/PermissionProvider', () => ({
usePermissionProvider: jest.fn().mockImplementation(() => ({
- getEntityPermissionByFqn: jest
- .fn()
- .mockImplementation(() => Promise.resolve(mockEntityPermissions)),
+ getEntityPermissionByFqn: mockEntityPermissionByFqn,
})),
}));
-jest.mock('rest/searchAPI', () => ({
- searchQuery: jest
- .fn()
- .mockImplementation(() => Promise.resolve(mockSearchQueryData)),
-}));
-
-jest.mock('components/MyData/LeftSidebar/LeftSidebar.component', () =>
- jest.fn().mockReturnValue(Sidebar
)
-);
-
jest.mock('rest/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)),
@@ -124,6 +171,11 @@ jest.mock('rest/databaseAPI', () => ({
.mockImplementation(() =>
Promise.resolve(mockPatchDatabaseSchemaDetailsData)
),
+ restoreDatabaseSchema: jest
+ .fn()
+ .mockImplementation(() =>
+ Promise.resolve(mockPatchDatabaseSchemaDetailsData)
+ ),
}));
jest.mock('../../AppState', () => ({
@@ -136,11 +188,6 @@ const mockParams = {
};
jest.mock('react-router-dom', () => ({
- Link: jest
- .fn()
- .mockImplementation(({ children }) => (
- {children}
- )),
useHistory: jest.fn().mockImplementation(() => ({
history: {
push: jest.fn(),
@@ -149,129 +196,113 @@ jest.mock('react-router-dom', () => ({
useParams: jest.fn().mockImplementation(() => mockParams),
}));
-jest.mock('components/containers/PageLayoutV1', () => {
- return jest.fn().mockImplementation(({ children }) => children);
-});
+describe('Tests for DatabaseSchemaPage', () => {
+ it('DatabaseSchemaPage should fetch permissions', () => {
+ render();
-describe.skip('Tests for DatabaseSchemaPage', () => {
- it('Page should render properly for "Tables" tab', async () => {
- act(() => {
- render(, {
- wrapper: MemoryRouter,
- });
- });
-
- const entityPageInfo = await screen.findByTestId('entityPageInfo');
- const tabsPane = await screen.findByTestId('tabs');
- const richTextEditorPreviewer = await screen.findAllByText(
- 'RichTextEditorPreviewer'
+ expect(mockEntityPermissionByFqn).toHaveBeenCalledWith(
+ 'databaseSchema',
+ mockParams.databaseSchemaFQN
);
- const description = await screen.findByText('Description');
- const nextPrevious = await screen.findByText('NextPrevious');
- const databaseSchemaTable = await screen.findByTestId(
- 'databaseSchema-tables'
- );
-
- expect(entityPageInfo).toBeInTheDocument();
- expect(tabsPane).toBeInTheDocument();
- expect(richTextEditorPreviewer).toHaveLength(10);
- expect(description).toBeInTheDocument();
- expect(nextPrevious).toBeInTheDocument();
- expect(databaseSchemaTable).toBeInTheDocument();
});
- it('Loader should be visible if the permissions are being fetched', async () => {
- await act(async () => {
- render(, {
- wrapper: MemoryRouter,
- });
+ it('DatabaseSchemaPage should not fetch details if permission is there', () => {
+ render();
- const loader = screen.getByText('Loader');
- const errorPlaceHolder = screen.queryByText('error-placeHolder');
-
- expect(loader).toBeInTheDocument();
- expect(errorPlaceHolder).toBeNull();
- });
-
- const entityPageInfo = await screen.findByTestId('entityPageInfo');
- const tabsPane = await screen.findByTestId('tabs');
-
- expect(entityPageInfo).toBeInTheDocument();
- expect(tabsPane).toBeInTheDocument();
+ expect(getDatabaseSchemaDetailsByFQN).not.toHaveBeenCalled();
+ expect(getStoredProceduresList).not.toHaveBeenCalled();
});
- it('Activity Feed List should render properly for "Activity Feeds" tab', async () => {
- mockParams.tab = 'activity_feed';
-
- await act(async () => {
- render(, {
- wrapper: MemoryRouter,
- });
- });
-
- const activityFeedList = await screen.findByTestId('ActivityFeedList');
-
- expect(activityFeedList).toBeInTheDocument();
- });
-
- it('ActivityThreadPanel should render properly after clicked on thread panel button', async () => {
- mockParams.tab = 'table';
- await act(async () => {
- render(, {
- wrapper: MemoryRouter,
- });
- });
-
- const description = await screen.findByText('Description');
-
- expect(description).toBeInTheDocument();
-
- act(() => {
- fireEvent.click(description);
- });
-
- const activityThreadPanel = await screen.findByText('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('error-placeHolder');
- 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 () => {
+ it('DatabaseSchemaPage should render permission placeholder if not have required permission', async () => {
(usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({
- getEntityPermissionByFqn: jest.fn().mockImplementation(() =>
- Promise.resolve({
- ...mockEntityPermissions,
- ViewAll: false,
- ViewBasic: false,
- })
- ),
+ getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({
+ ViewBasic: false,
+ })),
}));
await act(async () => {
- render(, {
- wrapper: MemoryRouter,
- });
+ render();
});
- const errorPlaceHolder = await screen.findByTestId('error-placeHolder');
+ expect(await screen.findByText('ErrorPlaceHolder')).toBeInTheDocument();
+ });
- expect(errorPlaceHolder).toBeInTheDocument();
+ it('DatabaseSchemaPage should fetch details with basic fields', async () => {
+ (usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({
+ getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({
+ ViewBasic: true,
+ })),
+ }));
+
+ await act(async () => {
+ render();
+ });
+
+ expect(getDatabaseSchemaDetailsByFQN).toHaveBeenCalledWith(
+ mockParams.databaseSchemaFQN,
+ ['owner', 'usageSummary', 'tags'],
+ 'include=all'
+ );
+ });
+
+ it('DatabaseSchemaPage should fetch storedProcedure with basic fields', async () => {
+ (usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({
+ getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({
+ ViewBasic: true,
+ })),
+ }));
+
+ await act(async () => {
+ render();
+ });
+
+ expect(getStoredProceduresList).toHaveBeenCalledWith({
+ databaseSchema: mockParams.databaseSchemaFQN,
+ fields: 'owner,tags,followers',
+ include: 'non-deleted',
+ limit: 0,
+ });
+ });
+
+ it('DatabaseSchemaPage should render page for ViewBasic permissions', async () => {
+ (usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({
+ getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({
+ ViewBasic: true,
+ })),
+ }));
+
+ await act(async () => {
+ render();
+ });
+
+ expect(getDatabaseSchemaDetailsByFQN).toHaveBeenCalledWith(
+ mockParams.databaseSchemaFQN,
+ ['owner', 'usageSummary', 'tags'],
+ 'include=all'
+ );
+
+ expect(await screen.findByText('testDataAssetsHeader')).toBeInTheDocument();
+ expect(await screen.findByTestId('tabs')).toBeInTheDocument();
+ expect(await screen.findByText('testSchemaTablesTab')).toBeInTheDocument();
+ });
+
+ it('DatabaseSchemaPage should render tables by default', async () => {
+ (usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({
+ getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({
+ ViewBasic: true,
+ })),
+ }));
+
+ await act(async () => {
+ render();
+ });
+
+ expect(getDatabaseSchemaDetailsByFQN).toHaveBeenCalledWith(
+ mockParams.databaseSchemaFQN,
+ ['owner', 'usageSummary', 'tags'],
+ 'include=all'
+ );
+
+ expect(await screen.findByText('testSchemaTablesTab')).toBeInTheDocument();
});
});
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
index 3afec746f42..82f5701c81e 100644
--- 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
@@ -254,3 +254,109 @@ export const mockEntityPermissions = {
EditDisplayName: true,
EditCustomFields: true,
};
+
+export const mockStoredProcedure = {
+ data: [
+ {
+ id: '5c225b39-0a20-4157-8085-5b22d0beea95',
+ name: 'update_dim_address_table',
+ fullyQualifiedName:
+ 'sample_data.ecommerce_db.shopify.update_dim_address_table',
+ description: 'This stored procedure updates dim_address table',
+ storedProcedureCode: {
+ code: 'CREATE OR REPLACE PROCEDURE output_message(message VARCHAR)\nRETURNS VARCHAR NOT NULL\nLANGUAGE SQL\nAS\n$$\nBEGIN\n RETURN message;\nEND;\n$$\n;',
+ },
+ version: 0.1,
+ updatedAt: 1694001765000,
+ updatedBy: 'admin',
+ href: 'http://localhost:8585/api/v1/storedProcedures/5c225b39-0a20-4157-8085-5b22d0beea95',
+ databaseSchema: {
+ id: '6745cd0c-e9d7-423e-80cc-accc3629811c',
+ type: 'databaseSchema',
+ name: 'shopify',
+ fullyQualifiedName: 'sample_data.ecommerce_db.shopify',
+ description:
+ 'This **mock** database contains schema related to shopify sales and orders with related dimension tables.',
+ deleted: false,
+ href: 'http://localhost:8585/api/v1/databaseSchemas/6745cd0c-e9d7-423e-80cc-accc3629811c',
+ },
+ database: {
+ id: 'a5f8e3b5-a23d-4d3c-9fca-ce7449493c5b',
+ 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/a5f8e3b5-a23d-4d3c-9fca-ce7449493c5b',
+ },
+ service: {
+ id: '7ac73a36-67f0-451e-a042-e91fb24ee6f1',
+ type: 'databaseService',
+ name: 'sample_data',
+ fullyQualifiedName: 'sample_data',
+ deleted: false,
+ href: 'http://localhost:8585/api/v1/services/databaseServices/7ac73a36-67f0-451e-a042-e91fb24ee6f1',
+ },
+ serviceType: 'CustomDatabase',
+ deleted: false,
+ followers: [],
+ tags: [],
+ },
+ {
+ id: '4651cccf-6c8a-49fc-804f-41a81bf6898b',
+ name: 'update_orders_table',
+ fullyQualifiedName:
+ 'sample_data.ecommerce_db.shopify.update_orders_table',
+ description:
+ 'This stored procedure is written java script to update the orders table',
+ storedProcedureCode: {
+ code: `create or replace procedure read_result_set()\n returns float not null\n language javascript\n
+ as \n $$ \n var my_sql_command = "select * from table1";\n var statement1 = snowflake.
+ createStatement( {sqlText: my_sql_command} );\n var result_set1 = statement1.execute();\n // Loop
+ through the results, processing one row at a time... \n while (result_set1.next()) {\n
+ var column1 = result_set1.getColumnValue(1);\n var column2 = result_set1.getColumnValue(2);\n
+ // Do something with the retrieved values...\n }\n return 0.0; // Replace with something more useful.\n $$\n ;`,
+ },
+ version: 0.1,
+ updatedAt: 1694001765116,
+ updatedBy: 'admin',
+ href: 'http://localhost:8585/api/v1/storedProcedures/4651cccf-6c8a-49fc-804f-41a81bf6898b',
+ databaseSchema: {
+ id: '6745cd0c-e9d7-423e-80cc-accc3629811c',
+ type: 'databaseSchema',
+ name: 'shopify',
+ fullyQualifiedName: 'sample_data.ecommerce_db.shopify',
+ description:
+ 'This **mock** database contains schema related to shopify sales and orders with related dimension tables.',
+ deleted: false,
+ href: 'http://localhost:8585/api/v1/databaseSchemas/6745cd0c-e9d7-423e-80cc-accc3629811c',
+ },
+ database: {
+ id: 'a5f8e3b5-a23d-4d3c-9fca-ce7449493c5b',
+ 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/a5f8e3b5-a23d-4d3c-9fca-ce7449493c5b',
+ },
+ service: {
+ id: '7ac73a36-67f0-451e-a042-e91fb24ee6f1',
+ type: 'databaseService',
+ name: 'sample_data',
+ fullyQualifiedName: 'sample_data',
+ deleted: false,
+ href: 'http://localhost:8585/api/v1/services/databaseServices/7ac73a36-67f0-451e-a042-e91fb24ee6f1',
+ },
+ serviceType: 'CustomDatabase',
+ deleted: false,
+ followers: [],
+ tags: [],
+ },
+ ],
+ paging: {
+ total: 2,
+ },
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/storedProceduresAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/storedProceduresAPI.ts
index 5a3682617fa..ea7cb229b93 100644
--- a/openmetadata-ui/src/main/resources/ui/src/rest/storedProceduresAPI.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/rest/storedProceduresAPI.ts
@@ -21,7 +21,7 @@ import { ServicePageData } from 'pages/ServiceDetailsPage/ServiceDetailsPage';
import { getURLWithQueryFields } from 'utils/APIUtils';
import APIClient from './index';
-interface ListStoredProcedureParams {
+export interface ListStoredProcedureParams {
databaseSchema?: string;
fields?: string;
after?: string;