feat(ui): supported version page in stored procedure (#13077)

* supported version page in stored procedure

* minor changes

* changes as per comments
This commit is contained in:
Ashish Gupta 2023-09-06 12:19:28 +05:30 committed by GitHub
parent c52af7eba0
commit 3d8c5cd36a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 565 additions and 3 deletions

View File

@ -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<ChangeDescription>(
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: <TabsLabel id={EntityTabs.CODE} name={t('label.code')} />,
children: (
<Row gutter={[0, 16]} wrap={false}>
<Col className="p-t-sm m-x-lg" flex="auto">
<Row gutter={[0, 16]}>
<Col span={24}>
<DescriptionV1
isVersionView
description={description}
entityType={EntityType.STORED_PROCEDURE}
/>
</Col>
</Row>
</Col>
<Col
className="entity-tag-right-panel-container"
data-testid="entity-right-panel"
flex="220px">
<Space className="w-full" direction="vertical" size="large">
{Object.keys(TagSource).map((tagType) => (
<TagsContainerV2
entityFqn={storedProcedureFQN}
entityType={EntityType.STORED_PROCEDURE}
key={tagType}
permission={false}
selectedTags={tags}
tagType={TagSource[tagType as TagSource]}
/>
))}
</Space>
</Col>
</Row>
),
},
{
key: EntityTabs.CUSTOM_PROPERTIES,
label: (
<TabsLabel
id={EntityTabs.CUSTOM_PROPERTIES}
name={t('label.custom-property-plural')}
/>
),
children: (
<CustomPropertyTable
isVersionView
entityDetails={
currentVersionData as CustomPropertyProps['entityDetails']
}
entityType={EntityType.STORED_PROCEDURE}
hasEditAccess={false}
hasPermission={entityPermissions.ViewAll}
/>
),
},
],
[description, storedProcedureFQN, currentVersionData, entityPermissions]
);
return (
<>
{isVersionLoading ? (
<Loader />
) : (
<div className={classNames('version-data')}>
<Row gutter={[0, 12]}>
<Col span={24}>
<DataAssetsVersionHeader
breadcrumbLinks={slashedTableName}
currentVersionData={currentVersionData}
deleted={deleted}
displayName={displayName}
entityType={EntityType.STORED_PROCEDURE}
ownerDisplayName={ownerDisplayName}
ownerRef={ownerRef}
serviceName={currentVersionData.service?.name}
tierDisplayName={tierDisplayName}
version={version}
onVersionClick={backHandler}
/>
</Col>
<Col span={24}>
<Tabs
defaultActiveKey={tab ?? EntityTabs.CODE}
items={tabItems}
onChange={handleTabChange}
/>
</Col>
</Row>
</div>
)}
<EntityVersionTimeLine
currentVersion={toString(version)}
versionHandler={versionHandler}
versionList={versionList}
onBack={backHandler}
/>
</>
);
};
export default StoredProcedureVersion;

View File

@ -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;
}

View File

@ -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(() => <div>DataAssetsVersionHeader</div>)
);
jest.mock('components/TabsLabel/TabsLabel.component', () =>
jest.fn().mockImplementation(({ name }) => <div>{name}</div>)
);
jest.mock('components/Tag/TagsContainerV2/TagsContainerV2', () =>
jest.fn().mockImplementation(() => <div>TagsContainerV2</div>)
);
jest.mock('components/common/CustomPropertyTable/CustomPropertyTable', () => ({
CustomPropertyTable: jest
.fn()
.mockImplementation(() => <div>CustomPropertyTable</div>),
}));
jest.mock('components/common/description/DescriptionV1', () =>
jest.fn().mockImplementation(() => <div>DescriptionV1</div>)
);
jest.mock('components/Entity/EntityVersionTimeLine/EntityVersionTimeLine', () =>
jest.fn().mockImplementation(() => <div>EntityVersionTimeLine</div>)
);
jest.mock('components/Loader/Loader', () =>
jest.fn().mockImplementation(() => <div>Loader</div>)
);
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(<StoredProcedureVersion {...storedProcedureVersionMockProps} />);
});
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(
<StoredProcedureVersion
{...storedProcedureVersionMockProps}
isVersionLoading
/>
);
});
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(<StoredProcedureVersion {...storedProcedureVersionMockProps} />);
});
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'
);
});
});

View File

@ -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<EntityUnion>({} 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 <Loader />;
return <Loader size="small" />;
}
return (

View File

@ -113,7 +113,7 @@ const TitleBreadcrumb: FunctionComponent<TitleBreadcrumbProps> = ({
<li
className="d-flex items-center breadcrumb-item"
data-testid="breadcrumb-link"
key={`${link.name + index}`}>
key={link.name}>
{link.imgSrc ? (
<img alt="" className="inline h-5 m-r-xs" src={link.imgSrc} />
) : null}

View File

@ -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,
};

View File

@ -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 (
<StoredProcedureVersion
backHandler={backHandler}
currentVersionData={currentVersionData}
deleted={currentVersionData.deleted}
entityPermissions={entityPermissions}
isVersionLoading={isVersionLoading}
owner={owner}
slashedTableName={slashedEntityName}
storedProcedureFQN={entityFQN}
tier={tier as TagLabel}
version={version}
versionHandler={versionHandler}
versionList={versionList}
/>
);
}
default:
return null;
}

View File

@ -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: