Fix custom property permission issue (#7126)

This commit is contained in:
Sachin Chaurasiya 2022-09-01 20:11:24 +05:30 committed by GitHub
parent 891603e72f
commit 77c40c97f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 98 additions and 421 deletions

View File

@ -1,180 +0,0 @@
/*
* Copyright 2021 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 { fireEvent, render } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Type } from '../../generated/entity/type';
import { Tab } from '../common/TabsPane/TabsPane';
import CustomEntityDetail from './CustomEntityDetail';
const mockData = {
id: '32f81349-d7d7-4a6a-8fc7-d767f233b674',
name: 'table',
fullyQualifiedName: 'table',
displayName: 'table',
description:
// eslint-disable-next-line max-len
'"This schema defines the Table entity. A Table organizes data in rows and columns and is defined by a Schema. OpenMetadata does not have a separate abstraction for Schema. Both Table and Schema are captured in this entity."',
category: 'entity',
nameSpace: 'data',
schema: '',
version: 0.1,
updatedAt: 1653626359971,
updatedBy: 'admin',
href: 'http://localhost:8585/api/v1/metadata/types/32f81349-d7d7-4a6a-8fc7-d767f233b674',
} as Type;
const mockPush = jest.fn();
const MOCK_HISTORY = {
push: mockPush,
};
jest.mock('react-router-dom', () => ({
useHistory: jest.fn().mockImplementation(() => MOCK_HISTORY),
}));
jest.mock('../../axiosAPIs/metadataTypeAPI', () => ({
getTypeByFQN: jest
.fn()
.mockImplementation(() => Promise.resolve({ data: mockData })),
}));
jest.mock('../containers/PageContainer', () => {
return jest
.fn()
.mockImplementation(({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
));
});
jest.mock('../../constants/constants', () => ({
getAddCustomPropertyPath: jest.fn().mockReturnValue('/custom-entity/table'),
}));
jest.mock('../common/TabsPane/TabsPane', () =>
jest.fn().mockImplementation(({ setActiveTab, tabs }) => {
return (
<div>
<nav
className="tw-flex tw-items-center tw-justify-between tw-gh-tabs-container tw-px-7"
data-testid="tabs"
id="tabs">
{tabs.map((tab: Tab) => (
<button
data-testid={tab.name}
key={tab.position}
onClick={() => setActiveTab?.(tab.position)}>
{tab.name}
</button>
))}
</nav>
</div>
);
})
);
jest.mock('../schema-editor/SchemaEditor', () =>
jest
.fn()
.mockReturnValue(<div data-testid="schema-editor">Schema Editor</div>)
);
jest.mock('./CustomPropertyTable', () => ({
CustomPropertyTable: jest
.fn()
.mockReturnValue(
<div data-testid="CustomPropertyTable">CustomPropertyTable</div>
),
}));
jest.mock('./LeftPanel', () => ({
LeftPanel: jest
.fn()
.mockReturnValue(<div data-testid="LeftPanel">LeftPanel</div>),
}));
describe('Test Custom Entity Detail Component', () => {
it('Should render custom entity component', async () => {
const { findByTestId } = render(
<CustomEntityDetail entityTypes={[mockData]} />,
{
wrapper: MemoryRouter,
}
);
const leftPanel = await findByTestId('LeftPanel');
const tabContainer = await findByTestId('tabs');
const schemaTab = await findByTestId('Schema');
const customPropertiesTab = await findByTestId('Custom Properties');
expect(leftPanel).toBeInTheDocument();
expect(tabContainer).toBeInTheDocument();
expect(schemaTab).toBeInTheDocument();
expect(customPropertiesTab).toBeInTheDocument();
});
it('Should render custom fields table if active tab is Custom Fields', async () => {
const { findByTestId } = render(
<CustomEntityDetail entityTypes={[mockData]} />,
{
wrapper: MemoryRouter,
}
);
const leftPanel = await findByTestId('LeftPanel');
const tabContainer = await findByTestId('tabs');
expect(leftPanel).toBeInTheDocument();
expect(tabContainer).toBeInTheDocument();
const customPropertiesTab = await findByTestId('Custom Properties');
expect(customPropertiesTab).toBeInTheDocument();
fireEvent.click(customPropertiesTab);
expect(await findByTestId('CustomPropertyTable')).toBeInTheDocument();
});
it('Should call history.push method on click of Add field button', async () => {
const { findByTestId } = render(
<CustomEntityDetail entityTypes={[mockData]} />,
{
wrapper: MemoryRouter,
}
);
const tabContainer = await findByTestId('tabs');
expect(tabContainer).toBeInTheDocument();
const customPropertiesTab = await findByTestId('Custom Properties');
expect(customPropertiesTab).toBeInTheDocument();
fireEvent.click(customPropertiesTab);
expect(await findByTestId('CustomPropertyTable')).toBeInTheDocument();
const addFieldButton = await findByTestId('add-field-button');
expect(addFieldButton).toBeInTheDocument();
fireEvent.click(addFieldButton);
expect(mockPush).toHaveBeenCalled();
});
});

View File

@ -1,181 +0,0 @@
/*
* Copyright 2021 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 { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isEmpty } from 'lodash';
import React, { FC, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { getTypeByFQN, updateType } from '../../axiosAPIs/metadataTypeAPI';
import { getAddCustomPropertyPath } from '../../constants/constants';
import { Type } from '../../generated/entity/type';
import jsonData from '../../jsons/en';
import { showErrorToast } from '../../utils/ToastUtils';
import { Button } from '../buttons/Button/Button';
import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder';
import TabsPane from '../common/TabsPane/TabsPane';
import PageContainer from '../containers/PageContainer';
import PageLayout from '../containers/PageLayout';
import SchemaEditor from '../schema-editor/SchemaEditor';
import { CustomPropertyTable } from './CustomPropertyTable';
import { LeftPanel } from './LeftPanel';
interface Props {
entityTypes: Array<Type>;
entityTypeFQN?: string;
}
const CustomEntityDetail: FC<Props> = ({ entityTypes, entityTypeFQN }) => {
const history = useHistory();
const [activeTab, setActiveTab] = useState<number>(1);
const [selectedEntityType, setSelectedEntityType] = useState<Type>(
{} as Type
);
const [selectedEntityTypeDetail, setSelectedEntityTypeDetail] =
useState<Type>({} as Type);
const fetchTypeDetail = (typeFQN: string) => {
getTypeByFQN(typeFQN)
.then((res) => {
setSelectedEntityTypeDetail(res);
})
.catch((err: AxiosError) => showErrorToast(err));
};
const onTabChange = (tab: number) => {
setActiveTab(tab);
};
const onEntityTypeSelect = (entityType: Type) => {
setSelectedEntityType(entityType);
};
const handleAddProperty = () => {
const path = getAddCustomPropertyPath(
selectedEntityTypeDetail.fullyQualifiedName as string
);
history.push(path);
};
const schemaCheck = activeTab === 2 && !isEmpty(selectedEntityTypeDetail);
const schemaValue = selectedEntityTypeDetail.schema || '{}';
const customPropertiesCheck =
activeTab === 1 && !isEmpty(selectedEntityTypeDetail);
const customProperties = selectedEntityTypeDetail.customProperties || [];
const tabs = [
{
name: 'Custom Properties',
isProtected: false,
position: 1,
count: customProperties.length,
},
{
name: 'Schema',
isProtected: false,
position: 2,
},
];
const componentCheck = Boolean(entityTypes.length);
const updateEntityType = (properties: Type['customProperties']) => {
const patch = compare(selectedEntityTypeDetail, {
...selectedEntityTypeDetail,
customProperties: properties,
});
updateType(selectedEntityTypeDetail.id as string, patch)
.then((res) => {
const { customProperties: properties } = res;
setSelectedEntityTypeDetail((prev) => ({
...prev,
customProperties: properties,
}));
})
.catch((err: AxiosError) => showErrorToast(err));
};
useEffect(() => {
if (entityTypes.length) {
const entityType =
entityTypes.find((type) => type.fullyQualifiedName === entityTypeFQN) ||
entityTypes[0];
onEntityTypeSelect(entityType);
}
}, [entityTypes, entityTypeFQN]);
useEffect(() => {
if (!isEmpty(selectedEntityType)) {
fetchTypeDetail(selectedEntityType.fullyQualifiedName as string);
}
}, [selectedEntityType]);
return (
<PageContainer>
{componentCheck ? (
<PageLayout
leftPanel={
<LeftPanel
selectedType={selectedEntityTypeDetail}
typeList={entityTypes}
/>
}>
<TabsPane
activeTab={activeTab}
setActiveTab={onTabChange}
tabs={tabs}
/>
<div className="tw-mt-4">
{schemaCheck && (
<div data-testid="entity-schema">
<SchemaEditor
className="tw-border tw-border-main tw-rounded-md tw-py-4"
editorClass="custom-entity-schema"
value={JSON.parse(schemaValue)}
/>
</div>
)}
{customPropertiesCheck && (
<div data-testid="entity-custom-fields">
<div className="tw-flex tw-justify-end">
<Button
className="tw-mb-4 tw-py-1 tw-px-2 tw-rounded"
data-testid="add-field-button"
size="custom"
theme="primary"
onClick={() => handleAddProperty()}>
Add Property
</Button>
</div>
<CustomPropertyTable
customProperties={customProperties}
updateEntityType={updateEntityType}
/>
</div>
)}
</div>
</PageLayout>
) : (
<ErrorPlaceHolder>
{jsonData['message']['no-custom-entity']}
</ErrorPlaceHolder>
)}
</PageContainer>
);
};
export default CustomEntityDetail;

View File

@ -60,6 +60,7 @@ const mockProperties = [
];
const mockProp = {
hasAccess: true,
customProperties: mockProperties,
updateEntityType: mockUpdateEntityType,
};

View File

@ -11,9 +11,11 @@
* limitations under the License.
*/
import { Tooltip } from 'antd';
import classNames from 'classnames';
import { isEmpty, uniqueId } from 'lodash';
import React, { FC, Fragment, useState } from 'react';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import { CustomProperty, Type } from '../../generated/entity/type';
import { getEntityName, isEven } from '../../utils/CommonUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
@ -22,6 +24,7 @@ import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
interface CustomPropertyTableProp {
hasAccess: boolean;
customProperties: CustomProperty[];
updateEntityType: (customProperties: Type['customProperties']) => void;
}
@ -31,6 +34,7 @@ type Operation = 'delete' | 'update' | 'no-operation';
export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
customProperties,
updateEntityType,
hasAccess,
}) => {
const [selectedProperty, setSelectedProperty] = useState<CustomProperty>(
{} as CustomProperty
@ -120,34 +124,42 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
</td>
<td className="tableBody-cell">
<div className="tw-flex">
<button
className="tw-cursor-pointer"
data-testid="edit-button"
onClick={() => {
setSelectedProperty(property);
setOperation('update');
}}>
<SVGIcons
alt="edit"
icon={Icons.EDIT}
title="Edit"
width="16px"
/>
</button>
<button
className="tw-cursor-pointer tw-ml-4"
data-testid="delete-button"
onClick={() => {
setSelectedProperty(property);
setOperation('delete');
}}>
<SVGIcons
alt="delete"
icon={Icons.DELETE}
title="Delete"
width="16px"
/>
</button>
<Tooltip
title={hasAccess ? 'Edit' : NO_PERMISSION_FOR_ACTION}>
<button
className="tw-cursor-pointer"
data-testid="edit-button"
disabled={!hasAccess}
onClick={() => {
setSelectedProperty(property);
setOperation('update');
}}>
<SVGIcons
alt="edit"
icon={Icons.EDIT}
title="Edit"
width="16px"
/>
</button>
</Tooltip>
<Tooltip
title={hasAccess ? 'Delete' : NO_PERMISSION_FOR_ACTION}>
<button
className="tw-cursor-pointer tw-ml-4"
data-testid="delete-button"
disabled={!hasAccess}
onClick={() => {
setSelectedProperty(property);
setOperation('delete');
}}>
<SVGIcons
alt="delete"
icon={Icons.DELETE}
title="Delete"
width="16px"
/>
</button>
</Tooltip>
</div>
</td>
</tr>

View File

@ -11,10 +11,10 @@
* limitations under the License.
*/
import { Col, Empty, Row } from 'antd';
import { Col, Empty, Row, Tooltip } from 'antd';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isEmpty, isUndefined } from 'lodash';
import { isUndefined } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { getTypeByFQN, updateType } from '../../axiosAPIs/metadataTypeAPI';
@ -24,15 +24,20 @@ import TabsPane from '../../components/common/TabsPane/TabsPane';
import { CustomPropertyTable } from '../../components/CustomEntityDetail/CustomPropertyTable';
import Loader from '../../components/Loader/Loader';
import { usePermissionProvider } from '../../components/PermissionProvider/PermissionProvider';
import {
OperationPermission,
ResourceEntity,
} from '../../components/PermissionProvider/PermissionProvider.interface';
import SchemaEditor from '../../components/schema-editor/SchemaEditor';
import { getAddCustomPropertyPath } from '../../constants/constants';
import { customAttributesPath } from '../../constants/globalSettings.constants';
import { NO_PERMISSION_TO_VIEW } from '../../constants/HelperTextUtil';
import { Operation } from '../../generated/entity/policies/policy';
import {
NO_PERMISSION_FOR_ACTION,
NO_PERMISSION_TO_VIEW,
} from '../../constants/HelperTextUtil';
import { Type } from '../../generated/entity/type';
import jsonData from '../../jsons/en';
import { getResourceEntityFromCustomProperty } from '../../utils/CustomPropertyUtils';
import { checkPermission } from '../../utils/PermissionsUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import './CustomPropertiesPageV1.less';
@ -49,18 +54,32 @@ const CustomEntityDetailV1 = () => {
const tabAttributePath =
customAttributesPath[tab as keyof typeof customAttributesPath];
const { permissions } = usePermissionProvider();
const { getEntityPermission } = usePermissionProvider();
const viewPermission = useMemo(() => {
return (
!isEmpty(permissions) &&
checkPermission(
Operation.ViewAll,
getResourceEntityFromCustomProperty(tab),
permissions
)
);
}, [permissions, tab]);
const [propertyPermission, setPropertyPermission] =
useState<OperationPermission>(DEFAULT_ENTITY_PERMISSION);
const fetchPermission = async () => {
try {
const response = await getEntityPermission(
ResourceEntity.TYPE,
selectedEntityTypeDetail.id as string
);
setPropertyPermission(response);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const viewPermission = useMemo(
() => propertyPermission.ViewAll,
[propertyPermission, tab]
);
const editPermission = useMemo(
() => propertyPermission.EditAll,
[propertyPermission, tab]
);
const fetchTypeDetail = async (typeFQN: string) => {
setIsLoading(true);
@ -126,6 +145,12 @@ const CustomEntityDetailV1 = () => {
}
}, [tab]);
useEffect(() => {
if (selectedEntityTypeDetail?.id) {
fetchPermission();
}
}, [selectedEntityTypeDetail]);
if (isLoading) {
return <Loader />;
}
@ -163,17 +188,22 @@ const CustomEntityDetailV1 = () => {
{activeTab === 1 && (
<div data-testid="entity-custom-fields">
<div className="tw-flex tw-justify-end">
<Button
className="tw-mb-4 tw-py-1 tw-px-2 tw-rounded"
data-testid="add-field-button"
size="custom"
theme="primary"
onClick={() => handleAddProperty()}>
Add Property
</Button>
<Tooltip
title={editPermission ? 'Add' : NO_PERMISSION_FOR_ACTION}>
<Button
className="tw-mb-4 tw-py-1 tw-px-2 tw-rounded"
data-testid="add-field-button"
disabled={!editPermission}
size="custom"
theme="primary"
onClick={() => handleAddProperty()}>
Add Property
</Button>
</Tooltip>
</div>
<CustomPropertyTable
customProperties={selectedEntityTypeDetail.customProperties || []}
hasAccess={editPermission}
updateEntityType={updateEntityType}
/>
</div>

View File

@ -466,14 +466,9 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
)}
path={ROUTES.BOTS_PROFILE}
/>
<AdminProtectedRoute
<Route
exact
component={AddCustomProperty}
hasPermission={checkPermission(
Operation.Create,
ResourceEntity.TYPE,
permissions
)}
path={ROUTES.ADD_CUSTOM_PROPERTY}
/>
<Route

View File

@ -267,7 +267,7 @@ const GlobalSettingRouter = () => {
component={CustomPropertiesPageV1}
hasPermission={checkPermission(
Operation.ViewAll,
ResourceEntity.ALL,
ResourceEntity.TYPE,
permissions
)}
path={getSettingCategoryPath(