mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-09 15:32:25 +00:00
fix: edit description permission for domain owner (#19475)
* fix: edit description permission for domain owner * fix: usage of permissions prop * test: fix unit test and add e2e test for edit description * refactor: remove editCustomAttributePermission and viewAllPermission props * fix: e2e test for edit description * fix: playwright tests * fix: update e2e test for edit description (cherry picked from commit 4059df8843afe472f27b3712a2a7ef562f1f5f10)
This commit is contained in:
parent
95501d850e
commit
4abe64087a
@ -10,7 +10,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import test, { expect } from '@playwright/test';
|
import base, { expect, Page } from '@playwright/test';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import { SidebarItem } from '../../constant/sidebar';
|
import { SidebarItem } from '../../constant/sidebar';
|
||||||
@ -40,11 +40,53 @@ import {
|
|||||||
import { sidebarClick } from '../../utils/sidebar';
|
import { sidebarClick } from '../../utils/sidebar';
|
||||||
import { performUserLogin, visitUserProfilePage } from '../../utils/user';
|
import { performUserLogin, visitUserProfilePage } from '../../utils/user';
|
||||||
|
|
||||||
test.describe('Domains', () => {
|
const user = new UserClass();
|
||||||
test.use({ storageState: 'playwright/.auth/admin.json' });
|
|
||||||
|
|
||||||
|
const domain = new Domain();
|
||||||
|
|
||||||
|
const test = base.extend<{
|
||||||
|
page: Page;
|
||||||
|
userPage: Page;
|
||||||
|
}>({
|
||||||
|
page: async ({ browser }, use) => {
|
||||||
|
const { page } = await performAdminLogin(browser);
|
||||||
|
await use(page);
|
||||||
|
await page.close();
|
||||||
|
},
|
||||||
|
userPage: async ({ browser }, use) => {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await user.login(page);
|
||||||
|
await use(page);
|
||||||
|
await page.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Domains', () => {
|
||||||
test.slow(true);
|
test.slow(true);
|
||||||
|
|
||||||
|
test.beforeAll('Setup pre-requests', async ({ browser }) => {
|
||||||
|
const { apiContext, afterAction } = await performAdminLogin(browser);
|
||||||
|
await user.create(apiContext);
|
||||||
|
|
||||||
|
await domain.create(apiContext);
|
||||||
|
|
||||||
|
await domain.patch({
|
||||||
|
apiContext,
|
||||||
|
patchData: [
|
||||||
|
{
|
||||||
|
op: 'add',
|
||||||
|
path: '/owners/0',
|
||||||
|
value: {
|
||||||
|
id: user.responseData.id,
|
||||||
|
type: 'user',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await afterAction();
|
||||||
|
});
|
||||||
|
|
||||||
test.beforeEach('Visit home page', async ({ page }) => {
|
test.beforeEach('Visit home page', async ({ page }) => {
|
||||||
await redirectToHomePage(page);
|
await redirectToHomePage(page);
|
||||||
});
|
});
|
||||||
@ -389,6 +431,41 @@ test.describe('Domains', () => {
|
|||||||
await afterAction();
|
await afterAction();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Domain owner should able to edit description of domain', async ({
|
||||||
|
page,
|
||||||
|
userPage,
|
||||||
|
}) => {
|
||||||
|
const { afterAction, apiContext } = await getApiContext(page);
|
||||||
|
try {
|
||||||
|
await sidebarClick(userPage, SidebarItem.DOMAIN);
|
||||||
|
await selectDomain(userPage, domain.data);
|
||||||
|
|
||||||
|
await expect(userPage.getByTestId('edit-description')).toBeInViewport();
|
||||||
|
|
||||||
|
await userPage.getByTestId('edit-description').click();
|
||||||
|
|
||||||
|
await expect(userPage.getByTestId('editor')).toBeInViewport();
|
||||||
|
|
||||||
|
const descriptionInputBox = '.om-block-editor[contenteditable="true"]';
|
||||||
|
|
||||||
|
await userPage.fill(descriptionInputBox, 'test description');
|
||||||
|
|
||||||
|
await userPage.getByTestId('save').click();
|
||||||
|
|
||||||
|
await userPage.waitForTimeout(3000);
|
||||||
|
|
||||||
|
const descriptionBox = '.om-block-editor[contenteditable="false"]';
|
||||||
|
|
||||||
|
await expect(userPage.locator(descriptionBox)).toHaveText(
|
||||||
|
'test description'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await domain?.delete(apiContext);
|
||||||
|
await user.delete(apiContext);
|
||||||
|
}
|
||||||
|
await afterAction();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Domains Rbac', () => {
|
test.describe('Domains Rbac', () => {
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { APIRequestContext } from '@playwright/test';
|
import { APIRequestContext } from '@playwright/test';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
import { uuid } from '../../utils/common';
|
import { uuid } from '../../utils/common';
|
||||||
|
|
||||||
type UserTeamRef = {
|
type UserTeamRef = {
|
||||||
@ -72,4 +73,28 @@ export class Domain {
|
|||||||
|
|
||||||
return response.body;
|
return response.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async patch({
|
||||||
|
apiContext,
|
||||||
|
patchData,
|
||||||
|
}: {
|
||||||
|
apiContext: APIRequestContext;
|
||||||
|
patchData: Operation[];
|
||||||
|
}) {
|
||||||
|
const response = await apiContext.patch(
|
||||||
|
`/api/v1/domains/${this.responseData?.id}`,
|
||||||
|
{
|
||||||
|
data: patchData,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json-patch+json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.responseData = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
entity: this.responseData,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { APIRequestContext } from '@playwright/test';
|
import { APIRequestContext } from '@playwright/test';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
import { uuid } from '../../utils/common';
|
import { uuid } from '../../utils/common';
|
||||||
import { Domain } from './Domain';
|
import { Domain } from './Domain';
|
||||||
|
|
||||||
@ -76,4 +77,28 @@ export class SubDomain {
|
|||||||
|
|
||||||
return response.body;
|
return response.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async patch({
|
||||||
|
apiContext,
|
||||||
|
patchData,
|
||||||
|
}: {
|
||||||
|
apiContext: APIRequestContext;
|
||||||
|
patchData: Operation[];
|
||||||
|
}) {
|
||||||
|
const response = await apiContext.patch(
|
||||||
|
`/api/v1/domains/${this.responseData?.id}`,
|
||||||
|
{
|
||||||
|
data: patchData,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json-patch+json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.responseData = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
entity: this.responseData,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -411,14 +411,9 @@ const DataProductsDetailsPage = ({
|
|||||||
children: (
|
children: (
|
||||||
<DocumentationTab
|
<DocumentationTab
|
||||||
domain={dataProduct}
|
domain={dataProduct}
|
||||||
editCustomAttributePermission={
|
|
||||||
(dataProductPermission.EditAll ||
|
|
||||||
dataProductPermission.EditCustomFields) &&
|
|
||||||
!isVersionsView
|
|
||||||
}
|
|
||||||
isVersionsView={isVersionsView}
|
isVersionsView={isVersionsView}
|
||||||
|
permissions={dataProductPermission}
|
||||||
type={DocumentationEntity.DATA_PRODUCT}
|
type={DocumentationEntity.DATA_PRODUCT}
|
||||||
viewAllPermission={dataProductPermission.ViewAll}
|
|
||||||
onExtensionUpdate={handelExtensionUpdate}
|
onExtensionUpdate={handelExtensionUpdate}
|
||||||
onUpdate={(data: Domain | DataProduct) =>
|
onUpdate={(data: Domain | DataProduct) =>
|
||||||
onUpdate(data as DataProduct)
|
onUpdate(data as DataProduct)
|
||||||
|
|||||||
@ -122,7 +122,7 @@ const DomainDetailsPage = ({
|
|||||||
}: DomainDetailsPageProps) => {
|
}: DomainDetailsPageProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = useForm();
|
const [form] = useForm();
|
||||||
const { getEntityPermission } = usePermissionProvider();
|
const { getEntityPermission, permissions } = usePermissionProvider();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { tab: activeTab, version } =
|
const { tab: activeTab, version } =
|
||||||
useParams<{ tab: string; version: string }>();
|
useParams<{ tab: string; version: string }>();
|
||||||
@ -202,21 +202,29 @@ const DomainDetailsPage = ({
|
|||||||
}, [domainPermission]);
|
}, [domainPermission]);
|
||||||
|
|
||||||
const addButtonContent = [
|
const addButtonContent = [
|
||||||
{
|
...(domainPermission.Create
|
||||||
label: t('label.asset-plural'),
|
? [
|
||||||
key: '1',
|
{
|
||||||
onClick: () => setAssetModalVisible(true),
|
label: t('label.asset-plural'),
|
||||||
},
|
key: '1',
|
||||||
{
|
onClick: () => setAssetModalVisible(true),
|
||||||
label: t('label.sub-domain-plural'),
|
},
|
||||||
key: '2',
|
{
|
||||||
onClick: () => setShowAddSubDomainModal(true),
|
label: t('label.sub-domain-plural'),
|
||||||
},
|
key: '2',
|
||||||
{
|
onClick: () => setShowAddSubDomainModal(true),
|
||||||
label: t('label.data-product-plural'),
|
},
|
||||||
key: '3',
|
]
|
||||||
onClick: () => setShowAddDataProductModal(true),
|
: []),
|
||||||
},
|
...(permissions.dataProduct.Create
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: t('label.data-product-plural'),
|
||||||
|
key: '3',
|
||||||
|
onClick: () => setShowAddDataProductModal(true),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const fetchSubDomains = useCallback(async () => {
|
const fetchSubDomains = useCallback(async () => {
|
||||||
@ -511,6 +519,7 @@ const DomainDetailsPage = ({
|
|||||||
<DocumentationTab
|
<DocumentationTab
|
||||||
domain={domain}
|
domain={domain}
|
||||||
isVersionsView={isVersionsView}
|
isVersionsView={isVersionsView}
|
||||||
|
permissions={domainPermission}
|
||||||
onUpdate={(data: Domain | DataProduct) => onUpdate(data as Domain)}
|
onUpdate={(data: Domain | DataProduct) => onUpdate(data as Domain)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -667,7 +676,7 @@ const DomainDetailsPage = ({
|
|||||||
</Col>
|
</Col>
|
||||||
<Col className="p-x-md" flex="320px">
|
<Col className="p-x-md" flex="320px">
|
||||||
<div style={{ textAlign: 'right' }}>
|
<div style={{ textAlign: 'right' }}>
|
||||||
{!isVersionsView && domainPermission.Create && (
|
{!isVersionsView && addButtonContent.length > 0 && (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
className="m-l-xs"
|
className="m-l-xs"
|
||||||
data-testid="domain-details-add-button-menu"
|
data-testid="domain-details-add-button-menu"
|
||||||
|
|||||||
@ -23,7 +23,6 @@ import DomainTypeSelectForm from '../../../../components/Domain/DomainTypeSelect
|
|||||||
import { DE_ACTIVE_COLOR } from '../../../../constants/constants';
|
import { DE_ACTIVE_COLOR } from '../../../../constants/constants';
|
||||||
import { EntityField } from '../../../../constants/Feeds.constants';
|
import { EntityField } from '../../../../constants/Feeds.constants';
|
||||||
import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../../../constants/ResizablePanel.constants';
|
import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../../../constants/ResizablePanel.constants';
|
||||||
import { usePermissionProvider } from '../../../../context/PermissionProvider/PermissionProvider';
|
|
||||||
import { ResourceEntity } from '../../../../context/PermissionProvider/PermissionProvider.interface';
|
import { ResourceEntity } from '../../../../context/PermissionProvider/PermissionProvider.interface';
|
||||||
import { EntityType, TabSpecificField } from '../../../../enums/entity.enum';
|
import { EntityType, TabSpecificField } from '../../../../enums/entity.enum';
|
||||||
import { DataProduct } from '../../../../generated/entity/domains/dataProduct';
|
import { DataProduct } from '../../../../generated/entity/domains/dataProduct';
|
||||||
@ -31,7 +30,6 @@ import {
|
|||||||
Domain,
|
Domain,
|
||||||
DomainType,
|
DomainType,
|
||||||
} from '../../../../generated/entity/domains/domain';
|
} from '../../../../generated/entity/domains/domain';
|
||||||
import { Operation } from '../../../../generated/entity/policies/policy';
|
|
||||||
import {
|
import {
|
||||||
ChangeDescription,
|
ChangeDescription,
|
||||||
EntityReference,
|
EntityReference,
|
||||||
@ -42,7 +40,6 @@ import {
|
|||||||
getEntityVersionByField,
|
getEntityVersionByField,
|
||||||
getOwnerVersionLabel,
|
getOwnerVersionLabel,
|
||||||
} from '../../../../utils/EntityVersionUtils';
|
} from '../../../../utils/EntityVersionUtils';
|
||||||
import { checkPermission } from '../../../../utils/PermissionsUtils';
|
|
||||||
import { CustomPropertyTable } from '../../../common/CustomPropertyTable/CustomPropertyTable';
|
import { CustomPropertyTable } from '../../../common/CustomPropertyTable/CustomPropertyTable';
|
||||||
import FormItemLabel from '../../../common/Form/FormItemLabel';
|
import FormItemLabel from '../../../common/Form/FormItemLabel';
|
||||||
import ResizablePanels from '../../../common/ResizablePanels/ResizablePanels';
|
import ResizablePanels from '../../../common/ResizablePanels/ResizablePanels';
|
||||||
@ -57,13 +54,11 @@ const DocumentationTab = ({
|
|||||||
domain,
|
domain,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onExtensionUpdate,
|
onExtensionUpdate,
|
||||||
editCustomAttributePermission,
|
|
||||||
viewAllPermission,
|
|
||||||
isVersionsView = false,
|
isVersionsView = false,
|
||||||
type = DocumentationEntity.DOMAIN,
|
type = DocumentationEntity.DOMAIN,
|
||||||
|
permissions,
|
||||||
}: DocumentationTabProps) => {
|
}: DocumentationTabProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { permissions } = usePermissionProvider();
|
|
||||||
const [isDescriptionEditable, setIsDescriptionEditable] =
|
const [isDescriptionEditable, setIsDescriptionEditable] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
const [editDomainType, setEditDomainType] = useState(false);
|
const [editDomainType, setEditDomainType] = useState(false);
|
||||||
@ -72,40 +67,40 @@ const DocumentationTab = ({
|
|||||||
? ResourceEntity.DOMAIN
|
? ResourceEntity.DOMAIN
|
||||||
: ResourceEntity.DATA_PRODUCT;
|
: ResourceEntity.DATA_PRODUCT;
|
||||||
|
|
||||||
const { editDescriptionPermission, editOwnerPermission, editAllPermission } =
|
const {
|
||||||
useMemo(() => {
|
editDescriptionPermission,
|
||||||
if (isVersionsView) {
|
editOwnerPermission,
|
||||||
return {
|
editAllPermission,
|
||||||
editDescriptionPermission: false,
|
editCustomAttributePermission,
|
||||||
editOwnerPermission: false,
|
viewAllPermission,
|
||||||
editAllPermission: false,
|
} = useMemo(() => {
|
||||||
};
|
if (isVersionsView) {
|
||||||
}
|
|
||||||
|
|
||||||
const editDescription = checkPermission(
|
|
||||||
Operation.EditDescription,
|
|
||||||
resourceType,
|
|
||||||
permissions
|
|
||||||
);
|
|
||||||
|
|
||||||
const editOwner = checkPermission(
|
|
||||||
Operation.EditOwners,
|
|
||||||
resourceType,
|
|
||||||
permissions
|
|
||||||
);
|
|
||||||
|
|
||||||
const editAll = checkPermission(
|
|
||||||
Operation.EditAll,
|
|
||||||
resourceType,
|
|
||||||
permissions
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
editDescriptionPermission: editDescription || editAll,
|
editDescriptionPermission: false,
|
||||||
editOwnerPermission: editOwner || editAll,
|
editOwnerPermission: false,
|
||||||
editAllPermission: editAll,
|
editAllPermission: false,
|
||||||
|
editCustomAttributePermission: false,
|
||||||
};
|
};
|
||||||
}, [permissions, isVersionsView, resourceType]);
|
}
|
||||||
|
|
||||||
|
const editDescription = permissions?.EditDescription;
|
||||||
|
|
||||||
|
const editOwner = permissions?.EditOwners;
|
||||||
|
|
||||||
|
const editAll = permissions?.EditAll;
|
||||||
|
|
||||||
|
const editCustomAttribute = permissions?.EditCustomFields;
|
||||||
|
|
||||||
|
const viewAll = permissions?.ViewAll;
|
||||||
|
|
||||||
|
return {
|
||||||
|
editDescriptionPermission: editAll || editDescription,
|
||||||
|
editOwnerPermission: editAll || editOwner,
|
||||||
|
editAllPermission: editAll,
|
||||||
|
editCustomAttributePermission: editAll || editCustomAttribute,
|
||||||
|
viewAllPermission: viewAll,
|
||||||
|
};
|
||||||
|
}, [permissions, isVersionsView, resourceType]);
|
||||||
|
|
||||||
const description = useMemo(
|
const description = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import { OperationPermission } from '../../../../context/PermissionProvider/PermissionProvider.interface';
|
||||||
import { DataProduct } from '../../../../generated/entity/domains/dataProduct';
|
import { DataProduct } from '../../../../generated/entity/domains/dataProduct';
|
||||||
import { Domain } from '../../../../generated/entity/domains/domain';
|
import { Domain } from '../../../../generated/entity/domains/domain';
|
||||||
|
|
||||||
@ -19,8 +20,7 @@ export interface DocumentationTabProps {
|
|||||||
isVersionsView?: boolean;
|
isVersionsView?: boolean;
|
||||||
type?: DocumentationEntity;
|
type?: DocumentationEntity;
|
||||||
onExtensionUpdate?: (updatedDataProduct: DataProduct) => Promise<void>;
|
onExtensionUpdate?: (updatedDataProduct: DataProduct) => Promise<void>;
|
||||||
editCustomAttributePermission?: boolean;
|
permissions?: OperationPermission;
|
||||||
viewAllPermission?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DocumentationEntity {
|
export enum DocumentationEntity {
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { render, screen } from '@testing-library/react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { MOCK_DOMAIN } from '../../../../mocks/Domains.mock';
|
import { MOCK_DOMAIN } from '../../../../mocks/Domains.mock';
|
||||||
|
import { MOCK_PERMISSIONS } from '../../../../mocks/Glossary.mock';
|
||||||
import DocumentationTab from './DocumentationTab.component';
|
import DocumentationTab from './DocumentationTab.component';
|
||||||
|
|
||||||
// Mock the onUpdate function
|
// Mock the onUpdate function
|
||||||
@ -23,6 +24,7 @@ const defaultProps = {
|
|||||||
domain: MOCK_DOMAIN,
|
domain: MOCK_DOMAIN,
|
||||||
onUpdate: mockOnUpdate,
|
onUpdate: mockOnUpdate,
|
||||||
isVersionsView: false,
|
isVersionsView: false,
|
||||||
|
permissions: MOCK_PERMISSIONS,
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('../../../common/EntityDescription/DescriptionV1', () => {
|
jest.mock('../../../common/EntityDescription/DescriptionV1', () => {
|
||||||
@ -33,10 +35,6 @@ jest.mock('../../../common/ProfilePicture/ProfilePicture', () =>
|
|||||||
jest.fn().mockReturnValue(<>ProfilePicture</>)
|
jest.fn().mockReturnValue(<>ProfilePicture</>)
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.mock('../../../../utils/PermissionsUtils', () => ({
|
|
||||||
checkPermission: jest.fn().mockReturnValue(true),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('DocumentationTab', () => {
|
describe('DocumentationTab', () => {
|
||||||
it('should render the initial content', () => {
|
it('should render the initial content', () => {
|
||||||
const { getByTestId } = render(<DocumentationTab {...defaultProps} />, {
|
const { getByTestId } = render(<DocumentationTab {...defaultProps} />, {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user