diff --git a/openmetadata-ui/src/main/resources/ui/src/components/StoredProcedureVersion/StoredProcedureVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/StoredProcedureVersion/StoredProcedureVersion.component.tsx new file mode 100644 index 00000000000..11dfa4ba21f --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/StoredProcedureVersion/StoredProcedureVersion.component.tsx @@ -0,0 +1,204 @@ +/* + * Copyright 2023 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 { Col, Row, Space, Tabs, TabsProps } from 'antd'; +import classNames from 'classnames'; +import { CustomPropertyTable } from 'components/common/CustomPropertyTable/CustomPropertyTable'; +import { CustomPropertyProps } from 'components/common/CustomPropertyTable/CustomPropertyTable.interface'; +import DescriptionV1 from 'components/common/description/DescriptionV1'; +import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; +import EntityVersionTimeLine from 'components/Entity/EntityVersionTimeLine/EntityVersionTimeLine'; +import Loader from 'components/Loader/Loader'; +import TabsLabel from 'components/TabsLabel/TabsLabel.component'; +import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2'; +import { getVersionPathWithTab } from 'constants/constants'; +import { EntityField } from 'constants/Feeds.constants'; +import { TagSource } from 'generated/type/tagLabel'; +import { toString } from 'lodash'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHistory, useParams } from 'react-router-dom'; +import { EntityTabs, EntityType } from '../../enums/entity.enum'; +import { ChangeDescription } from '../../generated/entity/data/table'; +import { + getCommonExtraInfoForVersionDetails, + getEntityVersionByField, + getEntityVersionTags, +} from '../../utils/EntityVersionUtils'; +import { StoredProcedureVersionProp } from './StoredProcedureVersion.interface'; +const StoredProcedureVersion = ({ + version, + currentVersionData, + isVersionLoading, + owner, + tier, + slashedTableName, + storedProcedureFQN, + versionList, + deleted = false, + backHandler, + versionHandler, + entityPermissions, +}: StoredProcedureVersionProp) => { + const { t } = useTranslation(); + const history = useHistory(); + const { tab } = useParams<{ tab: EntityTabs }>(); + const [changeDescription, setChangeDescription] = useState( + currentVersionData.changeDescription as ChangeDescription + ); + + const { ownerDisplayName, ownerRef, tierDisplayName } = useMemo( + () => getCommonExtraInfoForVersionDetails(changeDescription, owner, tier), + [changeDescription, owner, tier] + ); + + const { tags, description, displayName } = useMemo( + () => ({ + tags: getEntityVersionTags(currentVersionData, changeDescription), + description: getEntityVersionByField( + changeDescription, + EntityField.DESCRIPTION, + currentVersionData.description + ), + displayName: getEntityVersionByField( + changeDescription, + EntityField.DISPLAYNAME, + currentVersionData.displayName + ), + }), + [currentVersionData, changeDescription] + ); + + const handleTabChange = (activeKey: string) => { + history.push( + getVersionPathWithTab( + EntityType.STORED_PROCEDURE, + storedProcedureFQN, + String(version), + activeKey + ) + ); + }; + + useEffect(() => { + setChangeDescription( + currentVersionData.changeDescription as ChangeDescription + ); + }, [currentVersionData]); + + const tabItems: TabsProps['items'] = useMemo( + () => [ + { + key: EntityTabs.CODE, + label: , + children: ( + + + + + + + + + + + {Object.keys(TagSource).map((tagType) => ( + + ))} + + + + ), + }, + { + key: EntityTabs.CUSTOM_PROPERTIES, + label: ( + + ), + children: ( + + ), + }, + ], + [description, storedProcedureFQN, currentVersionData, entityPermissions] + ); + + return ( + <> + {isVersionLoading ? ( + + ) : ( +
+ + + + + + + + +
+ )} + + + + ); +}; + +export default StoredProcedureVersion; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/StoredProcedureVersion/StoredProcedureVersion.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/StoredProcedureVersion/StoredProcedureVersion.interface.ts new file mode 100644 index 00000000000..ceb3a6209f9 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/StoredProcedureVersion/StoredProcedureVersion.interface.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2023 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 { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface'; +import { StoredProcedure } from 'generated/entity/data/storedProcedure'; +import { VersionData } from 'pages/EntityVersionPage/EntityVersionPage.component'; +import { EntityHistory } from '../../generated/type/entityHistory'; +import { TagLabel } from '../../generated/type/tagLabel'; +import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface'; + +export interface StoredProcedureVersionProp { + version: string; + currentVersionData: VersionData; + isVersionLoading: boolean; + owner: StoredProcedure['owner']; + tier: TagLabel; + slashedTableName: TitleBreadcrumbProps['titleLinks']; + storedProcedureFQN: string; + versionList: EntityHistory; + deleted?: boolean; + backHandler: () => void; + versionHandler: (v: string) => void; + entityPermissions: OperationPermission; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/StoredProcedureVersion/StoredProcedureVersion.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/StoredProcedureVersion/StoredProcedureVersion.test.tsx new file mode 100644 index 00000000000..4cec371ef36 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/StoredProcedureVersion/StoredProcedureVersion.test.tsx @@ -0,0 +1,129 @@ +/* + * Copyright 2023 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, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { storedProcedureVersionMockProps } from 'mocks/StoredProcedureVersion.mock'; +import React from 'react'; +import StoredProcedureVersion from './StoredProcedureVersion.component'; + +const mockPush = jest.fn(); + +jest.mock( + 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader', + () => jest.fn().mockImplementation(() =>
DataAssetsVersionHeader
) +); + +jest.mock('components/TabsLabel/TabsLabel.component', () => + jest.fn().mockImplementation(({ name }) =>
{name}
) +); + +jest.mock('components/Tag/TagsContainerV2/TagsContainerV2', () => + jest.fn().mockImplementation(() =>
TagsContainerV2
) +); + +jest.mock('components/common/CustomPropertyTable/CustomPropertyTable', () => ({ + CustomPropertyTable: jest + .fn() + .mockImplementation(() =>
CustomPropertyTable
), +})); + +jest.mock('components/common/description/DescriptionV1', () => + jest.fn().mockImplementation(() =>
DescriptionV1
) +); + +jest.mock('components/Entity/EntityVersionTimeLine/EntityVersionTimeLine', () => + jest.fn().mockImplementation(() =>
EntityVersionTimeLine
) +); + +jest.mock('components/Loader/Loader', () => + jest.fn().mockImplementation(() =>
Loader
) +); + +jest.mock('react-router-dom', () => ({ + useHistory: jest.fn().mockImplementation(() => ({ + push: mockPush, + })), + useParams: jest.fn().mockReturnValue({ + tab: 'tables', + }), +})); + +describe('StoredProcedureVersion tests', () => { + it('Should render component properly if not loading', async () => { + await act(async () => { + render(); + }); + + const dataAssetsVersionHeader = screen.getByText('DataAssetsVersionHeader'); + const description = screen.getByText('DescriptionV1'); + const codeTabLabel = screen.getByText('label.code'); + const customPropertyTabLabel = screen.getByText( + 'label.custom-property-plural' + ); + const entityVersionTimeLine = screen.getByText('EntityVersionTimeLine'); + + expect(dataAssetsVersionHeader).toBeInTheDocument(); + expect(description).toBeInTheDocument(); + expect(codeTabLabel).toBeInTheDocument(); + expect(customPropertyTabLabel).toBeInTheDocument(); + expect(entityVersionTimeLine).toBeInTheDocument(); + }); + + it('Should display Loader if isVersionLoading is true', async () => { + await act(async () => { + render( + + ); + }); + + const loader = screen.getByText('Loader'); + const entityVersionTimeLine = screen.getByText('EntityVersionTimeLine'); + const dataAssetsVersionHeader = screen.queryByText( + 'DataAssetsVersionHeader' + ); + const codeTabLabel = screen.queryByText('label.code'); + const customPropertyTabLabel = screen.queryByText( + 'label.custom-property-plural' + ); + + expect(loader).toBeInTheDocument(); + expect(entityVersionTimeLine).toBeInTheDocument(); + expect(dataAssetsVersionHeader).toBeNull(); + expect(codeTabLabel).toBeNull(); + expect(customPropertyTabLabel).toBeNull(); + }); + + it('Should update url on click of tab', async () => { + await act(async () => { + render(); + }); + + const customPropertyTabLabel = screen.getByText( + 'label.custom-property-plural' + ); + + expect(customPropertyTabLabel).toBeInTheDocument(); + + await act(async () => { + userEvent.click(customPropertyTabLabel); + }); + + expect(mockPush).toHaveBeenCalledWith( + '/storedProcedure/sample_data.ecommerce_db.shopify.update_dim_address_table/versions/0.3/custom_properties' + ); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/EntityPopOverCard.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/EntityPopOverCard.tsx index 79dd5325f31..e73b3ba2c45 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/EntityPopOverCard.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/EntityPopOverCard.tsx @@ -28,10 +28,12 @@ import { getDatabaseDetailsByFQN, getDatabaseSchemaDetailsByFQN, } from 'rest/databaseAPI'; +import { getDataModelDetailsByFQN } from 'rest/dataModelsAPI'; import { getGlossariesByName, getGlossaryTermByFQN } from 'rest/glossaryAPI'; import { getMlModelByFQN } from 'rest/mlModelAPI'; import { getPipelineByFqn } from 'rest/pipelineAPI'; import { getContainerByFQN } from 'rest/storageAPI'; +import { getStoredProceduresDetailsByFQN } from 'rest/storedProceduresAPI'; import { getTableDetailsByFQN } from 'rest/tableAPI'; import { getTopicByFqn } from 'rest/topicsAPI'; import { getTableFQNFromColumnFQN } from 'utils/CommonUtils'; @@ -52,7 +54,7 @@ const PopoverContent: React.FC<{ entityType: string; }> = ({ entityFQN, entityType }) => { const [entityData, setEntityData] = useState({} as EntityUnion); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); const getData = useCallback(() => { const setEntityDetails = (entityDetail: EntityUnion) => { @@ -117,6 +119,16 @@ const PopoverContent: React.FC<{ break; + case EntityType.DASHBOARD_DATA_MODEL: + promise = getDataModelDetailsByFQN(entityFQN, fields); + + break; + + case EntityType.STORED_PROCEDURE: + promise = getStoredProceduresDetailsByFQN(entityFQN, fields); + + break; + default: break; } @@ -134,6 +146,8 @@ const PopoverContent: React.FC<{ .finally(() => { setLoading(false); }); + } else { + setLoading(false); } }, [entityType, entityFQN]); @@ -141,6 +155,7 @@ const PopoverContent: React.FC<{ const entityData = AppState.entityData[entityFQN]; if (entityData) { setEntityData(entityData); + setLoading(false); } else { getData(); } @@ -151,7 +166,7 @@ const PopoverContent: React.FC<{ }, [entityFQN]); if (loading) { - return ; + return ; } return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/title-breadcrumb/title-breadcrumb.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/title-breadcrumb/title-breadcrumb.component.tsx index 2f22b5591b1..76e1cfbcdf2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/title-breadcrumb/title-breadcrumb.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/title-breadcrumb/title-breadcrumb.component.tsx @@ -113,7 +113,7 @@ const TitleBreadcrumb: FunctionComponent = ({
  • + key={link.name}> {link.imgSrc ? ( ) : null} diff --git a/openmetadata-ui/src/main/resources/ui/src/mocks/StoredProcedureVersion.mock.ts b/openmetadata-ui/src/main/resources/ui/src/mocks/StoredProcedureVersion.mock.ts new file mode 100644 index 00000000000..d7ecb842667 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/mocks/StoredProcedureVersion.mock.ts @@ -0,0 +1,102 @@ +/* + * Copyright 2023 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 { StoredProcedureVersionProp } from 'components/StoredProcedureVersion/StoredProcedureVersion.interface'; +import { DatabaseServiceType, TableType } from 'generated/entity/data/table'; +import { ENTITY_PERMISSIONS } from 'mocks/Permissions.mock'; +import { + mockBackHandler, + mockOwner, + mockTier, + mockVersionHandler, + mockVersionList, +} from 'mocks/VersionCommon.mock'; + +const mockData = { + id: 'ab4f893b-c303-43d9-9375-3e620a670b02', + name: 'update_dim_address_table', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.update_dim_address_table', + description: + 'This is a raw product catalog table contains the product listing, price, seller etc.. represented in our online DB. ', + version: 0.2, + updatedAt: 1688442727895, + updatedBy: 'admin', + tableType: TableType.Regular, + columns: [], + owner: { + id: '38be030f-f817-4712-bc3b-ff7b9b9b805e', + type: 'user', + name: 'aaron_johnson0', + fullyQualifiedName: 'aaron_johnson0', + displayName: 'Aaron Johnson', + deleted: false, + }, + databaseSchema: { + id: '3f0d9c39-0926-4028-8070-65b0c03556cb', + 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, + }, + database: { + id: 'f085e133-e184-47c8-ada5-d7e005d3153b', + 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, + }, + service: { + id: 'e61069a9-29e3-49fa-a7f4-f5227ae50b72', + type: 'databaseService', + name: 'sample_data', + fullyQualifiedName: 'sample_data', + deleted: false, + }, + serviceType: DatabaseServiceType.BigQuery, + tags: [], + followers: [], + changeDescription: { + fieldsAdded: [ + { + name: 'owner', + newValue: + '{"id":"38be030f-f817-4712-bc3b-ff7b9b9b805e","type":"user","name":"aaron_johnson0","fullyQualifiedName":"aaron_johnson0","displayName":"Aaron Johnson","deleted":false}', + }, + ], + fieldsUpdated: [], + fieldsDeleted: [], + previousVersion: 0.1, + }, + deleted: false, +}; + +export const storedProcedureVersionMockProps: StoredProcedureVersionProp = { + version: '0.3', + currentVersionData: mockData, + isVersionLoading: false, + owner: mockOwner, + tier: mockTier, + slashedTableName: [], + storedProcedureFQN: + 'sample_data.ecommerce_db.shopify.update_dim_address_table', + versionList: mockVersionList, + deleted: false, + backHandler: mockBackHandler, + versionHandler: mockVersionHandler, + entityPermissions: ENTITY_PERMISSIONS, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/EntityVersionPage/EntityVersionPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/EntityVersionPage/EntityVersionPage.component.tsx index 22a7571d8da..31817d175c7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/EntityVersionPage/EntityVersionPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/EntityVersionPage/EntityVersionPage.component.tsx @@ -75,6 +75,7 @@ import { getDataModelDetailsPath, getMlModelDetailsPath, getPipelineDetailsPath, + getStoredProcedureDetailsPath, getTableTabPath, getTopicDetailsPath, getVersionPath, @@ -96,8 +97,15 @@ import { OperationPermission, ResourceEntity, } from 'components/PermissionProvider/PermissionProvider.interface'; +import StoredProcedureVersion from 'components/StoredProcedureVersion/StoredProcedureVersion.component'; import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum'; +import { StoredProcedure } from 'generated/entity/data/storedProcedure'; import { isEmpty } from 'lodash'; +import { + getStoredProceduresDetailsByFQN, + getStoredProceduresVersion, + getStoredProceduresVersionsList, +} from 'rest/storedProceduresAPI'; import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils'; import { getTierTags } from '../../utils/TableUtils'; import './EntityVersionPage.less'; @@ -109,6 +117,7 @@ export type VersionData = | Pipeline | Mlmodel | Container + | StoredProcedure | DashboardDataModel; const EntityVersionPage: FunctionComponent = () => { @@ -175,6 +184,11 @@ const EntityVersionPage: FunctionComponent = () => { break; + case EntityType.STORED_PROCEDURE: + history.push(getStoredProcedureDetailsPath(entityFQN, tab)); + + break; + default: break; } @@ -265,6 +279,11 @@ const EntityVersionPage: FunctionComponent = () => { break; } + case EntityType.STORED_PROCEDURE: { + await fetchResourcePermission(ResourceEntity.STORED_PROCEDURE); + + break; + } default: { break; } @@ -395,6 +414,18 @@ const EntityVersionPage: FunctionComponent = () => { break; } + case EntityType.STORED_PROCEDURE: { + const { id } = await getStoredProceduresDetailsByFQN(entityFQN, ''); + + setEntityId(id ?? ''); + + const versions = await getStoredProceduresVersionsList(id ?? ''); + + setVersionList(versions); + + break; + } + default: break; } @@ -512,6 +543,27 @@ const EntityVersionPage: FunctionComponent = () => { break; } + case EntityType.STORED_PROCEDURE: { + const currentVersion = await getStoredProceduresVersion( + id, + version + ); + + const { owner, tags = [] } = currentVersion; + + setEntityState( + tags, + owner, + currentVersion, + getEntityBreadcrumbs( + currentVersion, + EntityType.STORED_PROCEDURE + ) + ); + + break; + } + default: break; } @@ -663,6 +715,25 @@ const EntityVersionPage: FunctionComponent = () => { ); } + case EntityType.STORED_PROCEDURE: { + return ( + + ); + } + default: return null; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx index e6d8bd98ace..3436712d40a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx @@ -68,6 +68,7 @@ import { getMlModelDetailsPath, getPipelineDetailsPath, getServiceDetailsPath, + getStoredProcedureDetailPath, getTableDetailsPath, getTagsDetailsPath, getTopicDetailsPath, @@ -1037,6 +1038,10 @@ export const getEntityLinkFromType = ( return getContainerDetailPath(fullyQualifiedName); case EntityType.DATABASE: return getDatabaseDetailsPath(fullyQualifiedName); + case EntityType.DASHBOARD_DATA_MODEL: + return getDataModelDetailsPath(fullyQualifiedName); + case EntityType.STORED_PROCEDURE: + return getStoredProcedureDetailPath(fullyQualifiedName); default: return ''; } @@ -1161,6 +1166,7 @@ export const getEntityBreadcrumbs = ( entity: | SearchedDataProps['data'][number]['_source'] | DashboardDataModel + | StoredProcedure | Database | DatabaseSchema | DataAssetsWithoutServiceField, @@ -1169,6 +1175,7 @@ export const getEntityBreadcrumbs = ( ) => { switch (entityType) { case EntityType.TABLE: + case EntityType.STORED_PROCEDURE: return getBreadcrumbForTable(entity as Table, includeCurrent); case EntityType.GLOSSARY: case EntityType.GLOSSARY_TERM: