mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-02 04:13:17 +00:00
Feat(ui): add owners field in tags and classification (#21757)
* add owners field in classification * fix tests * add owners field in tags page * Tag Inherits owners from classification * Added loadTags.ts for owners field * add owners for tags page * fix tests * review comments * add owners field in create form * fix domain test --------- Co-authored-by: Ram Narayan Balaji <ramnarayanb3005@gmail.com> Co-authored-by: Mohit Yadav <105265192+mohityadav766@users.noreply.github.com>
This commit is contained in:
parent
d98c762501
commit
631c6f58fe
@ -83,10 +83,11 @@ public class TagRepository extends EntityRepository<Tag> {
|
||||
@Override
|
||||
public void setInheritedFields(Tag tag, Fields fields) {
|
||||
Classification parent =
|
||||
Entity.getEntity(CLASSIFICATION, tag.getClassification().getId(), "", ALL);
|
||||
Entity.getEntity(CLASSIFICATION, tag.getClassification().getId(), "owners", ALL);
|
||||
if (parent.getDisabled() != null && parent.getDisabled()) {
|
||||
tag.setDisabled(true);
|
||||
}
|
||||
inheritOwners(tag, fields, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -93,7 +93,7 @@ public class TagResource extends EntityResource<Tag, TagRepository> {
|
||||
private final ClassificationMapper classificationMapper = new ClassificationMapper();
|
||||
private final TagMapper mapper = new TagMapper();
|
||||
public static final String TAG_COLLECTION_PATH = "/v1/tags/";
|
||||
static final String FIELDS = "children,usageCount";
|
||||
static final String FIELDS = "owners,children,usageCount";
|
||||
|
||||
static class TagList extends ResultList<Tag> {
|
||||
/* Required for serde */
|
||||
@ -105,7 +105,7 @@ public class TagResource extends EntityResource<Tag, TagRepository> {
|
||||
|
||||
@Override
|
||||
protected List<MetadataOperation> getEntitySpecificOperations() {
|
||||
addViewOperation("children,usageCount", MetadataOperation.VIEW_BASIC);
|
||||
addViewOperation("owners,children,usageCount", MetadataOperation.VIEW_BASIC);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -18,11 +18,14 @@ import static jakarta.ws.rs.core.Response.Status.CREATED;
|
||||
import static jakarta.ws.rs.core.Response.Status.NOT_FOUND;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.openmetadata.common.utils.CommonUtil.listOf;
|
||||
import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
|
||||
import static org.openmetadata.service.Entity.FIELD_OWNERS;
|
||||
import static org.openmetadata.service.exception.CatalogExceptionMessage.entityNotFound;
|
||||
import static org.openmetadata.service.util.EntityUtil.fieldAdded;
|
||||
import static org.openmetadata.service.util.EntityUtil.fieldUpdated;
|
||||
import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS;
|
||||
import static org.openmetadata.service.util.TestUtils.UpdateType.MINOR_UPDATE;
|
||||
@ -510,6 +513,96 @@ public class TagResourceTest extends EntityResourceTest<Tag, CreateTag> {
|
||||
new TagLabel().withTagFQN(getTag.getFullyQualifiedName())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_ownerInheritance(TestInfo test) throws IOException {
|
||||
// Create a classification with owners
|
||||
CreateClassification create = classificationResourceTest.createRequest(getEntityName(test));
|
||||
Classification classification =
|
||||
classificationResourceTest.createAndCheckEntity(create, ADMIN_AUTH_HEADERS);
|
||||
assertTrue(
|
||||
listOrEmpty(classification.getOwners()).isEmpty(),
|
||||
"Classification should have no owners initially");
|
||||
|
||||
// Update classification owners as admin using PATCH
|
||||
String json = JsonUtils.pojoToJson(classification);
|
||||
classification.setOwners(List.of(USER1.getEntityReference()));
|
||||
ChangeDescription change = getChangeDescription(classification, MINOR_UPDATE);
|
||||
fieldAdded(change, FIELD_OWNERS, List.of(USER1.getEntityReference()));
|
||||
Classification createdClassification =
|
||||
classificationResourceTest.patchEntityAndCheck(
|
||||
classification, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
|
||||
assertEquals(
|
||||
1,
|
||||
listOrEmpty(createdClassification.getOwners()).size(),
|
||||
"Classification should have one owner");
|
||||
assertEquals(
|
||||
USER1.getId(),
|
||||
createdClassification.getOwners().getFirst().getId(),
|
||||
"Owner should match USER1");
|
||||
|
||||
// Create a tag under the classification
|
||||
String tagName = "TestTagForInheritance";
|
||||
CreateTag createTag =
|
||||
createRequest(tagName).withClassification(createdClassification.getName());
|
||||
Tag tag = createEntity(createTag, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Verify that the tag inherited owners from classification
|
||||
Tag getTag = getEntity(tag.getId(), FIELD_OWNERS, ADMIN_AUTH_HEADERS);
|
||||
assertNotNull(getTag.getOwners(), "Tag should have inherited owners");
|
||||
assertEquals(
|
||||
classification.getOwners().size(),
|
||||
getTag.getOwners().size(),
|
||||
"Tag should have inherited the correct number of owners");
|
||||
assertEquals(
|
||||
USER1_REF.getId(),
|
||||
getTag.getOwners().getFirst().getId(),
|
||||
"Tag should have inherited the correct owner");
|
||||
assertTrue(getTag.getOwners().getFirst().getInherited(), "Owner should be marked as inherited");
|
||||
|
||||
// Update classification owners - replace existing owner with a new one
|
||||
List<EntityReference> previousOwners = new ArrayList<>(classification.getOwners());
|
||||
String classificationJson = JsonUtils.pojoToJson(classification);
|
||||
classification.setOwners(List.of(USER2.getEntityReference()));
|
||||
change = getChangeDescription(classification, MINOR_UPDATE);
|
||||
fieldUpdated(change, FIELD_OWNERS, previousOwners, classification.getOwners());
|
||||
classification =
|
||||
classificationResourceTest.patchEntity(
|
||||
classification.getId(), classificationJson, classification, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Verify that the tag's owners were updated
|
||||
getTag = getEntity(tag.getId(), FIELD_OWNERS, ADMIN_AUTH_HEADERS);
|
||||
assertNotNull(getTag.getOwners(), "Tag should have updated owners");
|
||||
assertEquals(
|
||||
classification.getOwners().size(),
|
||||
getTag.getOwners().size(),
|
||||
"Tag should have inherited the correct number of owners after update");
|
||||
assertEquals(
|
||||
USER2_REF.getId(),
|
||||
getTag.getOwners().getFirst().getId(),
|
||||
"Tag should have the updated owner");
|
||||
assertTrue(getTag.getOwners().getFirst().getInherited(), "Owner should be marked as inherited");
|
||||
|
||||
// Test that tags with explicit owners don't get updated
|
||||
String tagWithOwnersName = "TagWithOwners";
|
||||
CreateTag createTagWithOwners =
|
||||
createRequest(tagWithOwnersName)
|
||||
.withClassification(classification.getName())
|
||||
.withOwners(List.of(USER1_REF));
|
||||
Tag tagWithOwners = createEntity(createTagWithOwners, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Verify that the tag is having both inherited owner USER2 as well as explicit owner USER1
|
||||
Tag getTagWithOwners = getEntity(tagWithOwners.getId(), FIELD_OWNERS, ADMIN_AUTH_HEADERS);
|
||||
assertNotNull(getTagWithOwners.getOwners(), "Tag should have owners");
|
||||
assertEquals(1, getTagWithOwners.getOwners().size(), "Tag should have one owner");
|
||||
assertEquals(
|
||||
USER1_REF.getId(),
|
||||
getTagWithOwners.getOwners().getFirst().getId(),
|
||||
"Tag should have kept its original owner");
|
||||
assertNull(
|
||||
getTagWithOwners.getOwners().getFirst().getInherited(),
|
||||
"Owner should not be marked as inherited");
|
||||
}
|
||||
|
||||
public Tag createTag(
|
||||
String name, String classification, String parentFqn, String... associatedTags)
|
||||
throws IOException {
|
||||
|
@ -48,6 +48,10 @@
|
||||
"domain" : {
|
||||
"description": "Fully qualified name of the domain the Table belongs to.",
|
||||
"type": "string"
|
||||
},
|
||||
"owners": {
|
||||
"description": "Owners of this glossary term.",
|
||||
"$ref": "../../type/entityReferenceList.json"
|
||||
}
|
||||
},
|
||||
"required": ["name", "description"],
|
||||
|
@ -86,6 +86,6 @@
|
||||
"$ref": "../../type/entityReferenceList.json"
|
||||
}
|
||||
},
|
||||
"required": ["name", "description"],
|
||||
"required": ["id", "name", "description"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
@ -103,9 +103,14 @@
|
||||
"dataProducts" : {
|
||||
"description": "List of data products this entity is part of.",
|
||||
"$ref" : "../../type/entityReferenceList.json"
|
||||
},
|
||||
"owners": {
|
||||
"description": "Owners of this glossary term.",
|
||||
"$ref": "../../type/entityReferenceList.json"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"description"
|
||||
],
|
||||
|
@ -14,6 +14,7 @@ import { expect, Page, test as base } from '@playwright/test';
|
||||
import { PolicyClass } from '../../support/access-control/PoliciesClass';
|
||||
import { RolesClass } from '../../support/access-control/RolesClass';
|
||||
import { Domain } from '../../support/domain/Domain';
|
||||
import { EntityTypeEndpoint } from '../../support/entity/Entity.interface';
|
||||
import { ClassificationClass } from '../../support/tag/ClassificationClass';
|
||||
import { TagClass } from '../../support/tag/TagClass';
|
||||
import { TeamClass } from '../../support/team/TeamClass';
|
||||
@ -25,6 +26,7 @@ import {
|
||||
redirectToHomePage,
|
||||
uuid,
|
||||
} from '../../utils/common';
|
||||
import { addMultiOwner, removeOwner } from '../../utils/entity';
|
||||
import {
|
||||
addAssetsToTag,
|
||||
editTagPageDescription,
|
||||
@ -38,6 +40,7 @@ import {
|
||||
verifyCertificationTagPageUI,
|
||||
verifyTagPageUI,
|
||||
} from '../../utils/tag';
|
||||
import { visitUserProfilePage } from '../../utils/user';
|
||||
|
||||
const adminUser = new UserClass();
|
||||
const dataConsumerUser = new UserClass();
|
||||
@ -51,6 +54,12 @@ const classification = new ClassificationClass({
|
||||
const tag = new TagClass({
|
||||
classification: classification.data.name,
|
||||
});
|
||||
const classification1 = new ClassificationClass();
|
||||
const tag1 = new TagClass({
|
||||
classification: classification1.data.name,
|
||||
});
|
||||
const user1 = new UserClass();
|
||||
const domain = new Domain();
|
||||
|
||||
const test = base.extend<{
|
||||
adminPage: Page;
|
||||
@ -93,7 +102,11 @@ base.beforeAll('Setup pre-requests', async ({ browser }) => {
|
||||
await dataStewardUser.setDataStewardRole(apiContext);
|
||||
await limitedAccessUser.create(apiContext);
|
||||
await classification.create(apiContext);
|
||||
await classification1.create(apiContext);
|
||||
await tag.create(apiContext);
|
||||
await tag1.create(apiContext);
|
||||
await user1.create(apiContext);
|
||||
await domain.create(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
@ -104,27 +117,17 @@ base.afterAll('Cleanup', async ({ browser }) => {
|
||||
await dataStewardUser.delete(apiContext);
|
||||
await limitedAccessUser.delete(apiContext);
|
||||
await classification.delete(apiContext);
|
||||
await classification1.delete(apiContext);
|
||||
await tag.delete(apiContext);
|
||||
await tag1.delete(apiContext);
|
||||
await user1.delete(apiContext);
|
||||
await domain.delete?.(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
test.describe('Tag Page with Admin Roles', () => {
|
||||
test.slow(true);
|
||||
|
||||
let domain: Domain;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
const { apiContext } = await performAdminLogin(browser);
|
||||
domain = new Domain();
|
||||
await domain.create(apiContext);
|
||||
});
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
const { apiContext, afterAction } = await performAdminLogin(browser);
|
||||
await domain.delete?.(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
test('Verify Tag UI', async ({ adminPage }) => {
|
||||
await verifyTagPageUI(adminPage, classification.data.name, tag);
|
||||
});
|
||||
@ -279,6 +282,47 @@ test.describe('Tag Page with Admin Roles', () => {
|
||||
domain.data.displayName
|
||||
);
|
||||
});
|
||||
|
||||
test('Verify Owner Add Delete', async ({ adminPage }) => {
|
||||
await tag1.visitPage(adminPage);
|
||||
const OWNER1 = user1.getUserName();
|
||||
|
||||
await addMultiOwner({
|
||||
page: adminPage,
|
||||
ownerNames: [OWNER1],
|
||||
activatorBtnDataTestId: 'add-owner',
|
||||
resultTestId: 'tag-owner-name',
|
||||
endpoint: EntityTypeEndpoint.Tag,
|
||||
isSelectableInsideForm: false,
|
||||
type: 'Users',
|
||||
});
|
||||
|
||||
// Verify in My Data page
|
||||
await visitUserProfilePage(adminPage, user1.responseData.name);
|
||||
await adminPage.waitForLoadState('networkidle');
|
||||
|
||||
const myDataRes = adminPage.waitForResponse(
|
||||
`/api/v1/search/query?q=*&index=all&from=0&size=15`
|
||||
);
|
||||
await adminPage.getByTestId('mydata').click();
|
||||
await myDataRes;
|
||||
|
||||
await expect(
|
||||
adminPage.getByTestId(
|
||||
`table-data-card_${tag1?.responseData?.fullyQualifiedName}`
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
await tag1.visitPage(adminPage);
|
||||
|
||||
await removeOwner({
|
||||
page: adminPage,
|
||||
endpoint: EntityTypeEndpoint.Tag,
|
||||
ownerName: OWNER1,
|
||||
type: 'Users',
|
||||
dataTestId: 'tag-owner-name',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Tag Page with Data Consumer Roles', () => {
|
||||
|
@ -12,9 +12,11 @@
|
||||
*/
|
||||
import { expect, Page, test } from '@playwright/test';
|
||||
import { SidebarItem } from '../../constant/sidebar';
|
||||
import { EntityTypeEndpoint } from '../../support/entity/Entity.interface';
|
||||
import { TableClass } from '../../support/entity/TableClass';
|
||||
import { ClassificationClass } from '../../support/tag/ClassificationClass';
|
||||
import { TagClass } from '../../support/tag/TagClass';
|
||||
import { UserClass } from '../../support/user/UserClass';
|
||||
import {
|
||||
clickOutside,
|
||||
createNewPage,
|
||||
@ -22,6 +24,7 @@ import {
|
||||
redirectToHomePage,
|
||||
uuid,
|
||||
} from '../../utils/common';
|
||||
import { addMultiOwner, removeOwner } from '../../utils/entity';
|
||||
import { sidebarClick } from '../../utils/sidebar';
|
||||
import { addTagToTableColumn, submitForm, validateForm } from '../../utils/tag';
|
||||
|
||||
@ -66,11 +69,20 @@ const tag = new TagClass({
|
||||
classification: classification.data.name,
|
||||
});
|
||||
|
||||
const classification1 = new ClassificationClass();
|
||||
const tag1 = new TagClass({
|
||||
classification: classification1.data.name,
|
||||
});
|
||||
const user1 = new UserClass();
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
const { apiContext, afterAction } = await createNewPage(browser);
|
||||
await table.create(apiContext);
|
||||
await classification.create(apiContext);
|
||||
await classification1.create(apiContext);
|
||||
await tag.create(apiContext);
|
||||
await tag1.create(apiContext);
|
||||
await user1.create(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
@ -78,7 +90,10 @@ test.afterAll(async ({ browser }) => {
|
||||
const { apiContext, afterAction } = await createNewPage(browser);
|
||||
await table.delete(apiContext);
|
||||
await classification.delete(apiContext);
|
||||
await classification1.delete(apiContext);
|
||||
await tag.delete(apiContext);
|
||||
await tag1.delete(apiContext);
|
||||
await user1.delete(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
@ -430,6 +445,9 @@ test.fixme('Classification Page', async ({ page }) => {
|
||||
await page.click('[data-testid="confirm-button"]');
|
||||
await deleteClassification;
|
||||
|
||||
await user1.visitPage(page);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-testid="data-summary-container"]')
|
||||
@ -486,3 +504,36 @@ test('Search tag using classification display name should work', async ({
|
||||
page.getByTestId(`tag-${tag.responseData.fullyQualifiedName}`)
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Verify Owner Add Delete', async ({ page }) => {
|
||||
await classification1.visitPage(page);
|
||||
const OWNER1 = user1.getUserName();
|
||||
|
||||
await addMultiOwner({
|
||||
page,
|
||||
ownerNames: [OWNER1],
|
||||
activatorBtnDataTestId: 'add-owner',
|
||||
resultTestId: 'classification-owner-name',
|
||||
endpoint: EntityTypeEndpoint.Classification,
|
||||
isSelectableInsideForm: false,
|
||||
type: 'Users',
|
||||
});
|
||||
|
||||
await page.getByTestId(tag1.data.name).click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await expect(
|
||||
page.locator(`[data-testid="tag-owner-name"]`).getByTestId(OWNER1)
|
||||
).toBeVisible();
|
||||
|
||||
await classification1.visitPage(page);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await removeOwner({
|
||||
page,
|
||||
endpoint: EntityTypeEndpoint.Classification,
|
||||
ownerName: OWNER1,
|
||||
type: 'Users',
|
||||
dataTestId: 'classification-owner-name',
|
||||
});
|
||||
});
|
||||
|
@ -43,6 +43,8 @@ export enum EntityTypeEndpoint {
|
||||
TestSuites = 'dataQuality/testSuites',
|
||||
Topic = 'topics',
|
||||
User = 'users',
|
||||
Classification = 'classifications',
|
||||
Tag = 'tags',
|
||||
}
|
||||
|
||||
export type EntityDataType = {
|
||||
|
@ -486,7 +486,10 @@ export const fillTagForm = async (adminPage: Page, domain: Domain) => {
|
||||
await adminPage.locator(descriptionBox).fill(NEW_TAG.description);
|
||||
await adminPage.fill('[data-testid="icon-url"]', NEW_TAG.icon);
|
||||
await adminPage.fill('[data-testid="tags_color-color-input"]', NEW_TAG.color);
|
||||
await adminPage.click('[data-testid="add-domain"]');
|
||||
|
||||
await adminPage.click(
|
||||
'[data-testid="modal-container"] [data-testid="add-domain"]'
|
||||
);
|
||||
await adminPage
|
||||
.getByTestId(`tag-${domain.responseData.fullyQualifiedName}`)
|
||||
.click();
|
||||
|
@ -11,7 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import Icon from '@ant-design/icons/lib/components/Icon';
|
||||
import { Button, Col, Row, Space, Tooltip, Typography } from 'antd';
|
||||
import { Button, Card, Col, Row, Space, Tooltip, Typography } from 'antd';
|
||||
import ButtonGroup from 'antd/lib/button/button-group';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { AxiosError } from 'axios';
|
||||
@ -31,12 +31,11 @@ import { ReactComponent as IconTag } from '../../../assets/svg/classification.sv
|
||||
import { ReactComponent as LockIcon } from '../../../assets/svg/closed-lock.svg';
|
||||
import { ReactComponent as VersionIcon } from '../../../assets/svg/ic-version.svg';
|
||||
import { DE_ACTIVE_COLOR } from '../../../constants/constants';
|
||||
import { EntityField } from '../../../constants/Feeds.constants';
|
||||
import { CustomizeEntityType } from '../../../constants/Customize.constants';
|
||||
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
|
||||
import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface';
|
||||
import { EntityType, TabSpecificField } from '../../../enums/entity.enum';
|
||||
import { ProviderType } from '../../../generated/api/classification/createClassification';
|
||||
import { ChangeDescription } from '../../../generated/entity/classification/classification';
|
||||
import { Classification } from '../../../generated/entity/classification/classification';
|
||||
import { Tag } from '../../../generated/entity/classification/tag';
|
||||
import { Operation } from '../../../generated/entity/policies/policy';
|
||||
import { Paging } from '../../../generated/type/paging';
|
||||
@ -46,10 +45,10 @@ import { useFqn } from '../../../hooks/useFqn';
|
||||
import { getTags } from '../../../rest/tagAPI';
|
||||
import {
|
||||
getClassificationExtraDropdownContent,
|
||||
getClassificationInfo,
|
||||
getTagsTableColumn,
|
||||
} from '../../../utils/ClassificationUtils';
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
import { getEntityVersionByField } from '../../../utils/EntityVersionUtils';
|
||||
import { checkPermission } from '../../../utils/PermissionsUtils';
|
||||
import {
|
||||
getClassificationDetailsPath,
|
||||
@ -63,7 +62,11 @@ import ManageButton from '../../common/EntityPageInfos/ManageButton/ManageButton
|
||||
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import { NextPreviousProps } from '../../common/NextPrevious/NextPrevious.interface';
|
||||
import Table from '../../common/Table/Table';
|
||||
import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider';
|
||||
import { DomainLabelV2 } from '../../DataAssets/DomainLabelV2/DomainLabelV2';
|
||||
import { OwnerLabelV2 } from '../../DataAssets/OwnerLabelV2/OwnerLabelV2';
|
||||
import EntityHeaderTitle from '../../Entity/EntityHeaderTitle/EntityHeaderTitle.component';
|
||||
import './classification-details.less';
|
||||
import { ClassificationDetailsProps } from './ClassificationDetails.interface';
|
||||
|
||||
const ClassificationDetails = forwardRef(
|
||||
@ -143,15 +146,17 @@ const ClassificationDetails = forwardRef(
|
||||
handlePageChange(currentPage);
|
||||
};
|
||||
|
||||
const currentVersion = useMemo(
|
||||
() => currentClassification?.version ?? '0.1',
|
||||
[currentClassification]
|
||||
);
|
||||
|
||||
const changeDescription = useMemo(
|
||||
() =>
|
||||
currentClassification?.changeDescription ?? ({} as ChangeDescription),
|
||||
[currentClassification]
|
||||
const {
|
||||
currentVersion,
|
||||
isClassificationDisabled,
|
||||
name,
|
||||
displayName,
|
||||
description,
|
||||
isTier,
|
||||
isSystemClassification,
|
||||
} = useMemo(
|
||||
() => getClassificationInfo(currentClassification, isVersionView),
|
||||
[currentClassification, isVersionView]
|
||||
);
|
||||
|
||||
const versionHandler = useCallback(() => {
|
||||
@ -165,83 +170,37 @@ const ClassificationDetails = forwardRef(
|
||||
);
|
||||
}, [currentVersion, tagCategoryName]);
|
||||
|
||||
const isTier = useMemo(
|
||||
() => currentClassification?.name === 'Tier',
|
||||
[currentClassification]
|
||||
);
|
||||
|
||||
const createTagPermission = useMemo(
|
||||
() =>
|
||||
checkPermission(Operation.Create, ResourceEntity.TAG, permissions) ||
|
||||
classificationPermissions.EditAll,
|
||||
[permissions, classificationPermissions]
|
||||
);
|
||||
|
||||
const editClassificationPermission = useMemo(
|
||||
() => classificationPermissions.EditAll,
|
||||
[classificationPermissions]
|
||||
);
|
||||
|
||||
const isClassificationDisabled = useMemo(
|
||||
() => currentClassification?.disabled ?? false,
|
||||
[currentClassification?.disabled]
|
||||
);
|
||||
|
||||
const handleUpdateDisplayName = async (data: {
|
||||
name: string;
|
||||
displayName?: string;
|
||||
}) => {
|
||||
if (
|
||||
!isUndefined(currentClassification) &&
|
||||
!isUndefined(handleUpdateClassification)
|
||||
) {
|
||||
return handleUpdateClassification({
|
||||
...currentClassification,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateDescription = async (updatedHTML: string) => {
|
||||
if (
|
||||
!isUndefined(currentClassification) &&
|
||||
!isUndefined(handleUpdateClassification)
|
||||
) {
|
||||
handleUpdateClassification({
|
||||
...currentClassification,
|
||||
description: updatedHTML,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnableDisableClassificationClick = useCallback(() => {
|
||||
if (
|
||||
!isUndefined(currentClassification) &&
|
||||
!isUndefined(handleUpdateClassification)
|
||||
) {
|
||||
handleUpdateClassification({
|
||||
...currentClassification,
|
||||
disabled: !isClassificationDisabled,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
currentClassification,
|
||||
handleUpdateClassification,
|
||||
isClassificationDisabled,
|
||||
]);
|
||||
|
||||
const editDescriptionPermission = useMemo(
|
||||
() =>
|
||||
!isVersionView &&
|
||||
!isClassificationDisabled &&
|
||||
(classificationPermissions.EditAll ||
|
||||
classificationPermissions.EditDescription),
|
||||
[classificationPermissions, isVersionView]
|
||||
);
|
||||
|
||||
const isSystemClassification = useMemo(
|
||||
() => currentClassification?.provider === ProviderType.System,
|
||||
[currentClassification]
|
||||
const {
|
||||
editClassificationPermission,
|
||||
editDescriptionPermission,
|
||||
createPermission,
|
||||
deletePermission,
|
||||
editDisplayNamePermission,
|
||||
} = useMemo(
|
||||
() => ({
|
||||
editClassificationPermission: classificationPermissions.EditAll,
|
||||
editDescriptionPermission:
|
||||
!isVersionView &&
|
||||
!isClassificationDisabled &&
|
||||
(classificationPermissions.EditAll ||
|
||||
classificationPermissions.EditDescription),
|
||||
createPermission:
|
||||
!isVersionView &&
|
||||
(checkPermission(Operation.Create, ResourceEntity.TAG, permissions) ||
|
||||
classificationPermissions.EditAll),
|
||||
deletePermission:
|
||||
classificationPermissions.Delete && !isSystemClassification,
|
||||
editDisplayNamePermission:
|
||||
classificationPermissions.EditAll ||
|
||||
classificationPermissions.EditDisplayName,
|
||||
}),
|
||||
[
|
||||
permissions,
|
||||
classificationPermissions,
|
||||
isVersionView,
|
||||
isClassificationDisabled,
|
||||
isSystemClassification,
|
||||
]
|
||||
);
|
||||
|
||||
const headerBadge = useMemo(
|
||||
@ -255,25 +214,6 @@ const ClassificationDetails = forwardRef(
|
||||
[isSystemClassification, currentClassification]
|
||||
);
|
||||
|
||||
const createPermission = useMemo(
|
||||
() =>
|
||||
!isVersionView &&
|
||||
(createTagPermission || classificationPermissions.EditAll),
|
||||
[classificationPermissions, createTagPermission, isVersionView]
|
||||
);
|
||||
|
||||
const deletePermission = useMemo(
|
||||
() => classificationPermissions.Delete && !isSystemClassification,
|
||||
[classificationPermissions, isSystemClassification]
|
||||
);
|
||||
|
||||
const editDisplayNamePermission = useMemo(
|
||||
() =>
|
||||
classificationPermissions.EditAll ||
|
||||
classificationPermissions.EditDisplayName,
|
||||
[classificationPermissions]
|
||||
);
|
||||
|
||||
const showDisableOption = useMemo(
|
||||
() => !isTier && isSystemClassification && editClassificationPermission,
|
||||
[isTier, isSystemClassification, editClassificationPermission]
|
||||
@ -291,6 +231,40 @@ const ClassificationDetails = forwardRef(
|
||||
]
|
||||
);
|
||||
|
||||
const handleUpdateDisplayName = async (data: {
|
||||
name: string;
|
||||
displayName?: string;
|
||||
}) => {
|
||||
if (!isUndefined(currentClassification)) {
|
||||
return handleUpdateClassification?.({
|
||||
...currentClassification,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateDescription = async (updatedHTML: string) => {
|
||||
if (!isUndefined(currentClassification)) {
|
||||
handleUpdateClassification?.({
|
||||
...currentClassification,
|
||||
description: updatedHTML,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnableDisableClassificationClick = useCallback(() => {
|
||||
if (!isUndefined(currentClassification)) {
|
||||
handleUpdateClassification?.({
|
||||
...currentClassification,
|
||||
disabled: !isClassificationDisabled,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
currentClassification,
|
||||
handleUpdateClassification,
|
||||
isClassificationDisabled,
|
||||
]);
|
||||
|
||||
const addTagButtonToolTip = useMemo(() => {
|
||||
if (isClassificationDisabled) {
|
||||
return t('message.disabled-classification-actions-message');
|
||||
@ -338,36 +312,6 @@ const ClassificationDetails = forwardRef(
|
||||
]
|
||||
);
|
||||
|
||||
const name = useMemo(() => {
|
||||
return isVersionView
|
||||
? getEntityVersionByField(
|
||||
changeDescription,
|
||||
EntityField.NAME,
|
||||
currentClassification?.name
|
||||
)
|
||||
: currentClassification?.name;
|
||||
}, [currentClassification, changeDescription]);
|
||||
|
||||
const displayName = useMemo(() => {
|
||||
return isVersionView
|
||||
? getEntityVersionByField(
|
||||
changeDescription,
|
||||
EntityField.DISPLAYNAME,
|
||||
currentClassification?.displayName
|
||||
)
|
||||
: currentClassification?.displayName;
|
||||
}, [currentClassification, changeDescription]);
|
||||
|
||||
const description = useMemo(() => {
|
||||
return isVersionView
|
||||
? getEntityVersionByField(
|
||||
changeDescription,
|
||||
EntityField.DESCRIPTION,
|
||||
currentClassification?.description
|
||||
)
|
||||
: currentClassification?.description;
|
||||
}, [currentClassification, changeDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentClassification?.fullyQualifiedName && !isAddingTag) {
|
||||
fetchClassificationChildren(currentClassification.fullyQualifiedName);
|
||||
@ -472,51 +416,77 @@ const ClassificationDetails = forwardRef(
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<div className="m-b-sm m-t-xs" data-testid="description-container">
|
||||
<DescriptionV1
|
||||
className={classNames({
|
||||
'opacity-60': isClassificationDisabled,
|
||||
})}
|
||||
description={description}
|
||||
entityName={getEntityName(currentClassification)}
|
||||
entityType={EntityType.CLASSIFICATION}
|
||||
hasEditAccess={editDescriptionPermission}
|
||||
isDescriptionExpanded={isEmpty(tags)}
|
||||
showCommentsIcon={false}
|
||||
onDescriptionUpdate={handleUpdateDescription}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
className={classNames({
|
||||
'opacity-60': isClassificationDisabled,
|
||||
})}
|
||||
columns={tableColumn}
|
||||
customPaginationProps={{
|
||||
currentPage,
|
||||
isLoading: isTagsLoading,
|
||||
pageSize,
|
||||
paging,
|
||||
showPagination,
|
||||
pagingHandler: handleTagsPageChange,
|
||||
onShowSizeChange: handlePageSizeChange,
|
||||
}}
|
||||
data-testid="table"
|
||||
dataSource={tags}
|
||||
loading={isTagsLoading}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<ErrorPlaceHolder
|
||||
className="m-y-md"
|
||||
placeholderText={t('message.no-tags-description')}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
pagination={false}
|
||||
rowClassName={(record) => (record.disabled ? 'opacity-60' : '')}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
/>
|
||||
<GenericProvider<Classification>
|
||||
data={currentClassification as Classification}
|
||||
isVersionView={isVersionView}
|
||||
permissions={classificationPermissions}
|
||||
type={EntityType.CLASSIFICATION as CustomizeEntityType}
|
||||
onUpdate={(updatedData: Classification) =>
|
||||
Promise.resolve(handleUpdateClassification?.(updatedData))
|
||||
}>
|
||||
<Row className="m-t-md" gutter={16}>
|
||||
<Col span={18}>
|
||||
<Card className="classification-details-card">
|
||||
<div className="m-b-sm" data-testid="description-container">
|
||||
<DescriptionV1
|
||||
wrapInCard
|
||||
className={classNames({
|
||||
'opacity-60': isClassificationDisabled,
|
||||
})}
|
||||
description={description}
|
||||
entityName={getEntityName(currentClassification)}
|
||||
entityType={EntityType.CLASSIFICATION}
|
||||
hasEditAccess={editDescriptionPermission}
|
||||
isDescriptionExpanded={isEmpty(tags)}
|
||||
showCommentsIcon={false}
|
||||
onDescriptionUpdate={handleUpdateDescription}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
className={classNames({
|
||||
'opacity-60': isClassificationDisabled,
|
||||
})}
|
||||
columns={tableColumn}
|
||||
customPaginationProps={{
|
||||
currentPage,
|
||||
isLoading: isTagsLoading,
|
||||
pageSize,
|
||||
paging,
|
||||
showPagination,
|
||||
pagingHandler: handleTagsPageChange,
|
||||
onShowSizeChange: handlePageSizeChange,
|
||||
}}
|
||||
data-testid="table"
|
||||
dataSource={tags}
|
||||
loading={isTagsLoading}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<ErrorPlaceHolder
|
||||
className="m-y-md"
|
||||
placeholderText={t('message.no-tags-description')}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
pagination={false}
|
||||
rowClassName={(record) =>
|
||||
record.disabled ? 'opacity-60' : ''
|
||||
}
|
||||
rowKey="id"
|
||||
scroll={{ x: true }}
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="d-flex flex-column gap-5">
|
||||
<DomainLabelV2 showDomainHeading />
|
||||
<OwnerLabelV2 dataTestId="classification-owner-name" />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</GenericProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*/
|
||||
.classification-details-card {
|
||||
> .ant-card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { EntityType } from '../../../enums/entity.enum';
|
||||
import { Classification } from '../../../generated/entity/classification/classification';
|
||||
import { Tag } from '../../../generated/entity/classification/tag';
|
||||
import { APICollection } from '../../../generated/entity/data/apiCollection';
|
||||
import { APIEndpoint } from '../../../generated/entity/data/apiEndpoint';
|
||||
@ -106,4 +107,5 @@ export type MapPatchAPIResponse = {
|
||||
[EntityType.METRIC]: Metric;
|
||||
[EntityType.TAG]: Tag;
|
||||
[EntityType.DOMAIN]: Domain;
|
||||
[EntityType.CLASSIFICATION]: Classification;
|
||||
};
|
||||
|
@ -27,6 +27,7 @@ const mockForm = jest.fn().mockReturnValue(<p data-testid="form">data</p>);
|
||||
const mockInitionalData = {
|
||||
name: '',
|
||||
description: '',
|
||||
id: '123',
|
||||
};
|
||||
|
||||
describe.skip('Test FormModal component', () => {
|
||||
|
@ -15,12 +15,12 @@ import { DefaultOptionType } from 'antd/lib/select';
|
||||
import { PagingResponse } from 'Models';
|
||||
import { Tag } from '../../../generated/entity/classification/tag';
|
||||
import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm';
|
||||
import { TagSource } from '../../../generated/type/tagLabel';
|
||||
import { TagLabel, TagSource } from '../../../generated/type/tagLabel';
|
||||
|
||||
export type SelectOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
data?: Tag | GlossaryTerm;
|
||||
data?: Tag | GlossaryTerm | TagLabel;
|
||||
};
|
||||
|
||||
export interface AsyncSelectListProps {
|
||||
|
@ -36,6 +36,7 @@ import React, {
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
|
||||
import { TAG_START_WITH } from '../../../constants/Tag.constants';
|
||||
import { Tag } from '../../../generated/entity/classification/tag';
|
||||
import { LabelType } from '../../../generated/entity/data/table';
|
||||
import { Paging } from '../../../generated/type/paging';
|
||||
import { TagLabel } from '../../../generated/type/tagLabel';
|
||||
@ -83,7 +84,7 @@ const AsyncSelectList: FC<AsyncSelectListProps & SelectProps> = ({
|
||||
|
||||
const filteredData = data.filter((item) => {
|
||||
const isFiltered = filterOptions.includes(
|
||||
item.data?.fullyQualifiedName ?? ''
|
||||
(item.data as Tag)?.fullyQualifiedName ?? ''
|
||||
);
|
||||
if (isFiltered) {
|
||||
count = optionFilteredCount + 1;
|
||||
@ -212,7 +213,7 @@ const AsyncSelectList: FC<AsyncSelectListProps & SelectProps> = ({
|
||||
const { label, onClose } = data;
|
||||
const tagLabel = getTagDisplay(label as string);
|
||||
const tag = {
|
||||
tagFQN: selectedTag?.data.fullyQualifiedName,
|
||||
tagFQN: (selectedTag?.data as Tag)?.fullyQualifiedName,
|
||||
...pick(
|
||||
selectedTag?.data,
|
||||
'description',
|
||||
|
@ -37,6 +37,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as ArrowIcon } from '../../../assets/svg/ic-arrow-down.svg';
|
||||
import { PAGE_SIZE_LARGE, TEXT_BODY_COLOR } from '../../../constants/constants';
|
||||
import { TAG_START_WITH } from '../../../constants/Tag.constants';
|
||||
import { Tag } from '../../../generated/entity/classification/tag';
|
||||
import { Glossary } from '../../../generated/entity/data/glossary';
|
||||
import { LabelType } from '../../../generated/entity/data/table';
|
||||
import { TagLabel } from '../../../generated/type/tagLabel';
|
||||
@ -150,7 +151,7 @@ const TreeAsyncSelectList: FC<Omit<AsyncSelectListProps, 'fetchOptions'>> = ({
|
||||
const { value, onClose } = data;
|
||||
const tagLabel = getTagDisplay(value as string);
|
||||
const tag = {
|
||||
tagFQN: selectedTag?.data.fullyQualifiedName,
|
||||
tagFQN: (selectedTag?.data as Tag)?.fullyQualifiedName,
|
||||
...pick(
|
||||
selectedTag?.data,
|
||||
'description',
|
||||
|
@ -44,6 +44,10 @@ export interface CreateTag {
|
||||
*/
|
||||
mutuallyExclusive?: boolean;
|
||||
name: string;
|
||||
/**
|
||||
* Owners of this glossary term.
|
||||
*/
|
||||
owners?: EntityReference[];
|
||||
/**
|
||||
* Fully qualified name of the parent tag. When null, the term is at the root of the
|
||||
* classification.
|
||||
@ -53,6 +57,62 @@ export interface CreateTag {
|
||||
style?: Style;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owners of this glossary term.
|
||||
*
|
||||
* This schema defines the EntityReferenceList type used for referencing an entity.
|
||||
* EntityReference is used for capturing relationships from one entity to another. For
|
||||
* example, a table has an attribute called database of type EntityReference that captures
|
||||
* the relationship of a table `belongs to a` database.
|
||||
*
|
||||
* This schema defines the EntityReference type used for referencing an entity.
|
||||
* EntityReference is used for capturing relationships from one entity to another. For
|
||||
* example, a table has an attribute called database of type EntityReference that captures
|
||||
* the relationship of a table `belongs to a` database.
|
||||
*/
|
||||
export interface EntityReference {
|
||||
/**
|
||||
* If true the entity referred to has been soft-deleted.
|
||||
*/
|
||||
deleted?: boolean;
|
||||
/**
|
||||
* Optional description of entity.
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* Display Name that identifies this entity.
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* Fully qualified name of the entity instance. For entities such as tables, databases
|
||||
* fullyQualifiedName is returned in this field. For entities that don't have name hierarchy
|
||||
* such as `user` and `team` this will be same as the `name` field.
|
||||
*/
|
||||
fullyQualifiedName?: string;
|
||||
/**
|
||||
* Link to the entity resource.
|
||||
*/
|
||||
href?: string;
|
||||
/**
|
||||
* Unique identifier that identifies an entity instance.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* If true the relationship indicated by this entity reference is inherited from the parent
|
||||
* entity.
|
||||
*/
|
||||
inherited?: boolean;
|
||||
/**
|
||||
* Name of the entity instance.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* Entity type/class name - Examples: `database`, `table`, `metrics`, `databaseService`,
|
||||
* `dashboardService`...
|
||||
*/
|
||||
type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of provider of an entity. Some entities are provided by the `system`. Some are
|
||||
* entities created and provided by the `user`. Typically `system` provide entities can't be
|
||||
|
@ -153,6 +153,10 @@ export interface CreateTagRequest {
|
||||
*/
|
||||
mutuallyExclusive?: boolean;
|
||||
name: string;
|
||||
/**
|
||||
* Owners of this glossary term.
|
||||
*/
|
||||
owners?: EntityReference[];
|
||||
/**
|
||||
* Fully qualified name of the parent tag. When null, the term is at the root of the
|
||||
* classification.
|
||||
|
@ -51,7 +51,7 @@ export interface Classification {
|
||||
/**
|
||||
* Unique identifier of this entity instance.
|
||||
*/
|
||||
id?: string;
|
||||
id: string;
|
||||
/**
|
||||
* Change that lead to this version of the entity.
|
||||
*/
|
||||
|
@ -68,7 +68,7 @@ export interface Tag {
|
||||
/**
|
||||
* Unique identifier of this entity instance.
|
||||
*/
|
||||
id?: string;
|
||||
id: string;
|
||||
/**
|
||||
* Change that lead to this version of the entity.
|
||||
*/
|
||||
@ -86,6 +86,10 @@ export interface Tag {
|
||||
* Name of the tag.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Owners of this glossary term.
|
||||
*/
|
||||
owners?: EntityReference[];
|
||||
/**
|
||||
* Reference to the parent tag. When null, the term is at the root of the Classification.
|
||||
*/
|
||||
|
@ -158,7 +158,7 @@ describe('TagPage', () => {
|
||||
// Verify initial API calls
|
||||
await waitFor(() => {
|
||||
expect(getTagByFqn).toHaveBeenCalledWith('PII.NonSensitive', {
|
||||
fields: 'domain',
|
||||
fields: ['domain', 'owners'],
|
||||
});
|
||||
expect(searchData).toHaveBeenCalled();
|
||||
});
|
||||
@ -177,7 +177,7 @@ describe('TagPage', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getTagByFqn).toHaveBeenCalledWith('Certification.Gold', {
|
||||
fields: 'domain',
|
||||
fields: ['domain', 'owners'],
|
||||
});
|
||||
expect(searchData).toHaveBeenCalled();
|
||||
});
|
||||
|
@ -12,6 +12,7 @@
|
||||
*/
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Dropdown,
|
||||
@ -38,7 +39,6 @@ import { ReactComponent as EditIcon } from '../../assets/svg/edit-new.svg';
|
||||
import { ReactComponent as IconDelete } from '../../assets/svg/ic-delete.svg';
|
||||
import { ReactComponent as IconDropdown } from '../../assets/svg/menu.svg';
|
||||
import { ReactComponent as StyleIcon } from '../../assets/svg/style.svg';
|
||||
import { DomainLabel } from '../../components/common/DomainLabel/DomainLabel.component';
|
||||
import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1';
|
||||
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import Loader from '../../components/common/Loader/Loader';
|
||||
@ -48,7 +48,10 @@ import StatusBadge from '../../components/common/StatusBadge/StatusBadge.compone
|
||||
import { StatusType } from '../../components/common/StatusBadge/StatusBadge.interface';
|
||||
import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component';
|
||||
import { TitleBreadcrumbProps } from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.interface';
|
||||
import { GenericProvider } from '../../components/Customization/GenericProvider/GenericProvider';
|
||||
import { AssetSelectionModal } from '../../components/DataAssets/AssetsSelectionModal/AssetSelectionModal';
|
||||
import { DomainLabelV2 } from '../../components/DataAssets/DomainLabelV2/DomainLabelV2';
|
||||
import { OwnerLabelV2 } from '../../components/DataAssets/OwnerLabelV2/OwnerLabelV2';
|
||||
import { EntityHeader } from '../../components/Entity/EntityHeader/EntityHeader.component';
|
||||
import EntitySummaryPanel from '../../components/Explore/EntitySummaryPanel/EntitySummaryPanel.component';
|
||||
import { EntityDetailsObjectInterface } from '../../components/Explore/ExplorePage.interface';
|
||||
@ -65,6 +68,7 @@ import {
|
||||
DE_ACTIVE_COLOR,
|
||||
ROUTES,
|
||||
} from '../../constants/constants';
|
||||
import { CustomizeEntityType } from '../../constants/Customize.constants';
|
||||
import { TAGS_DOCS } from '../../constants/docs.constants';
|
||||
import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../constants/ResizablePanel.constants';
|
||||
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
|
||||
@ -218,7 +222,7 @@ const TagPage = () => {
|
||||
setIsLoading(true);
|
||||
if (tagFqn) {
|
||||
const response = await getTagByFqn(tagFqn, {
|
||||
fields: TabSpecificField.DOMAIN,
|
||||
fields: [TabSpecificField.DOMAIN, TabSpecificField.OWNERS],
|
||||
});
|
||||
setTagItem(response);
|
||||
}
|
||||
@ -433,46 +437,38 @@ const TagPage = () => {
|
||||
label: <TabsLabel id={TagTabs.OVERVIEW} name={t('label.overview')} />,
|
||||
key: 'overview',
|
||||
children: (
|
||||
<ResizablePanels
|
||||
className="tag-height-with-resizable-panel"
|
||||
firstPanel={{
|
||||
className: 'tag-resizable-panel-container',
|
||||
children: (
|
||||
<div className="tag-overview-tab">
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<DescriptionV1
|
||||
removeBlur
|
||||
description={tagItem?.description}
|
||||
entityName={getEntityName(tagItem)}
|
||||
entityType={EntityType.TAG}
|
||||
hasEditAccess={editDescriptionPermission}
|
||||
showActions={!tagItem?.deleted}
|
||||
showCommentsIcon={false}
|
||||
onDescriptionUpdate={onDescriptionUpdate}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<GenericProvider<Tag>
|
||||
data={tagItem as Tag}
|
||||
isVersionView={false}
|
||||
permissions={tagPermissions}
|
||||
type={EntityType.TAG as CustomizeEntityType}
|
||||
onUpdate={(updatedData: Tag) =>
|
||||
Promise.resolve(updateTag(updatedData))
|
||||
}>
|
||||
<Row gutter={16}>
|
||||
<Col span={18}>
|
||||
<Card className="card-padding-md">
|
||||
<DescriptionV1
|
||||
removeBlur
|
||||
wrapInCard
|
||||
description={tagItem?.description}
|
||||
entityName={getEntityName(tagItem)}
|
||||
entityType={EntityType.TAG}
|
||||
hasEditAccess={editDescriptionPermission}
|
||||
showActions={!tagItem?.deleted}
|
||||
showCommentsIcon={false}
|
||||
onDescriptionUpdate={onDescriptionUpdate}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="d-flex flex-column gap-5">
|
||||
<DomainLabelV2 showDomainHeading />
|
||||
<OwnerLabelV2 dataTestId="tag-owner-name" />
|
||||
</div>
|
||||
),
|
||||
...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL,
|
||||
}}
|
||||
secondPanel={{
|
||||
children: tagItem ? (
|
||||
<DomainLabel
|
||||
showDomainHeading
|
||||
domain={tagItem.domain}
|
||||
entityFqn={tagItem.fullyQualifiedName ?? ''}
|
||||
entityId={tagItem.id ?? ''}
|
||||
entityType={EntityType.TAG}
|
||||
hasPermission={editTagsPermission}
|
||||
/>
|
||||
) : null,
|
||||
...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL,
|
||||
className:
|
||||
'entity-resizable-right-panel-container tag-resizable-panel-container',
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</GenericProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -624,7 +620,7 @@ const TagPage = () => {
|
||||
{haveAssetEditPermission && (
|
||||
<Col className="p-x-md">
|
||||
<div className="d-flex self-end">
|
||||
{!isCertificationClassification && (
|
||||
{!isCertificationClassification && !tagItem.disabled && (
|
||||
<Button
|
||||
data-testid="data-classification-add-button"
|
||||
type="primary"
|
||||
@ -672,7 +668,7 @@ const TagPage = () => {
|
||||
<Tabs
|
||||
destroyInactiveTabPane
|
||||
activeKey={activeTab}
|
||||
className="tabs-new"
|
||||
className="tabs-new tag-page-tabs"
|
||||
items={tabItems}
|
||||
onChange={activeTabHandler}
|
||||
/>
|
||||
|
@ -15,3 +15,9 @@
|
||||
.tag-height-with-resizable-panel {
|
||||
height: @tag-page-height;
|
||||
}
|
||||
|
||||
.tag-page-tabs.ant-tabs.tabs-new {
|
||||
.ant-tabs-tabpane {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ jest.mock('../../components/common/RichTextEditor/RichTextEditor', () => {
|
||||
|
||||
jest.mock('../../utils/CommonUtils', () => ({
|
||||
isUrlFriendlyName: jest.fn().mockReturnValue(true),
|
||||
getCountBadge: jest.fn().mockReturnValue(''),
|
||||
}));
|
||||
|
||||
const mockCancel = jest.fn();
|
||||
|
@ -12,11 +12,12 @@
|
||||
*/
|
||||
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Modal, Typography } from 'antd';
|
||||
import { Button, Form, Modal, Space, Typography } from 'antd';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DomainLabel } from '../../components/common/DomainLabel/DomainLabel.component';
|
||||
import { EntityAttachmentProvider } from '../../components/common/EntityDescription/EntityAttachmentProvider/EntityAttachmentProvider';
|
||||
import { OwnerLabel } from '../../components/common/OwnerLabel/OwnerLabel.component';
|
||||
import { VALIDATION_MESSAGES } from '../../constants/constants';
|
||||
import {
|
||||
HEX_COLOR_CODE_REGEX,
|
||||
@ -32,7 +33,7 @@ import {
|
||||
FormItemLayout,
|
||||
HelperTextType,
|
||||
} from '../../interface/FormUtils.interface';
|
||||
import { generateFormFields } from '../../utils/formUtils';
|
||||
import { generateFormFields, getField } from '../../utils/formUtils';
|
||||
import { RenameFormProps, SubmitProps } from './TagsPage.interface';
|
||||
|
||||
const TagsForm = ({
|
||||
@ -61,6 +62,12 @@ const TagsForm = ({
|
||||
'mutuallyExclusive',
|
||||
form
|
||||
);
|
||||
const selectedOwners =
|
||||
Form.useWatch<EntityReference | EntityReference[]>('owners', form) ?? [];
|
||||
|
||||
const ownersList = Array.isArray(selectedOwners)
|
||||
? selectedOwners
|
||||
: [selectedOwners];
|
||||
const { activeDomainEntityRef } = useDomainStore();
|
||||
|
||||
useEffect(() => {
|
||||
@ -105,6 +112,56 @@ const TagsForm = ({
|
||||
[isEditing, isClassification, permissions]
|
||||
);
|
||||
|
||||
const ownerField: FieldProp = {
|
||||
name: 'owners',
|
||||
id: 'root/owner',
|
||||
required: false,
|
||||
label: t('label.owner-plural'),
|
||||
type: FieldTypes.USER_TEAM_SELECT,
|
||||
props: {
|
||||
hasPermission: true,
|
||||
children: (
|
||||
<Button
|
||||
data-testid="add-owner"
|
||||
icon={<PlusOutlined style={{ color: 'white', fontSize: '12px' }} />}
|
||||
size="small"
|
||||
type="primary"
|
||||
/>
|
||||
),
|
||||
multiple: { user: true, team: false },
|
||||
},
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
formItemProps: {
|
||||
valuePropName: 'owners',
|
||||
trigger: 'onUpdate',
|
||||
},
|
||||
};
|
||||
|
||||
const domainField: FieldProp = {
|
||||
name: 'domain',
|
||||
id: 'root/domain',
|
||||
required: false,
|
||||
label: t('label.domain'),
|
||||
type: FieldTypes.DOMAIN_SELECT,
|
||||
props: {
|
||||
selectedDomain: activeDomainEntityRef,
|
||||
children: (
|
||||
<Button
|
||||
data-testid="add-domain"
|
||||
icon={<PlusOutlined style={{ color: 'white', fontSize: '12px' }} />}
|
||||
size="small"
|
||||
type="primary"
|
||||
/>
|
||||
),
|
||||
},
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
formItemProps: {
|
||||
valuePropName: 'selectedDomain',
|
||||
trigger: 'onUpdate',
|
||||
initialValue: activeDomainEntityRef,
|
||||
},
|
||||
};
|
||||
|
||||
const formFields: FieldProp[] = [
|
||||
{
|
||||
name: 'name',
|
||||
@ -228,30 +285,6 @@ const TagsForm = ({
|
||||
},
|
||||
] as FieldProp[])
|
||||
: []),
|
||||
{
|
||||
name: 'domain',
|
||||
id: 'root/domain',
|
||||
required: false,
|
||||
label: t('label.domain'),
|
||||
type: FieldTypes.DOMAIN_SELECT,
|
||||
props: {
|
||||
selectedDomain: activeDomainEntityRef,
|
||||
children: (
|
||||
<Button
|
||||
data-testid="add-domain"
|
||||
icon={<PlusOutlined style={{ color: 'white', fontSize: '12px' }} />}
|
||||
size="small"
|
||||
type="primary"
|
||||
/>
|
||||
),
|
||||
},
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
formItemProps: {
|
||||
valuePropName: 'selectedDomain',
|
||||
trigger: 'onUpdate',
|
||||
initialValue: activeDomainEntityRef,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleSave = async (data: SubmitProps) => {
|
||||
@ -307,18 +340,29 @@ const TagsForm = ({
|
||||
validateMessages={VALIDATION_MESSAGES}
|
||||
onFinish={handleSave}>
|
||||
{generateFormFields(formFields)}
|
||||
<div className="m-y-xs">
|
||||
{getField(ownerField)}
|
||||
{Boolean(ownersList.length) && (
|
||||
<Space wrap data-testid="owner-container" size={[8, 8]}>
|
||||
<OwnerLabel owners={ownersList} />
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
<div className="m-t-xss">
|
||||
{getField(domainField)}
|
||||
{selectedDomain && (
|
||||
<DomainLabel
|
||||
domain={selectedDomain}
|
||||
entityFqn=""
|
||||
entityId=""
|
||||
entityType={
|
||||
isClassification ? EntityType.CLASSIFICATION : EntityType.TAG
|
||||
}
|
||||
hasPermission={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
{selectedDomain && (
|
||||
<DomainLabel
|
||||
domain={selectedDomain}
|
||||
entityFqn=""
|
||||
entityId=""
|
||||
entityType={
|
||||
isClassification ? EntityType.CLASSIFICATION : EntityType.TAG
|
||||
}
|
||||
hasPermission={false}
|
||||
/>
|
||||
)}
|
||||
</EntityAttachmentProvider>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -43,7 +43,7 @@ export interface RenameFormProps {
|
||||
isTier: boolean;
|
||||
onCancel: () => void;
|
||||
header: string;
|
||||
initialValues?: Tag;
|
||||
initialValues?: Omit<Tag, 'id'>;
|
||||
onSubmit: (value: SubmitProps) => Promise<void>;
|
||||
showMutuallyExclusive?: boolean;
|
||||
isClassification?: boolean;
|
||||
|
@ -65,6 +65,7 @@ const mockCategory = [
|
||||
version: 0.1,
|
||||
updatedAt: 1649665563400,
|
||||
updatedBy: 'admin',
|
||||
owners: [],
|
||||
href: 'http://localhost:8585/api/v1/tags/PersonalData',
|
||||
usageCount: 0,
|
||||
children: [
|
||||
@ -108,6 +109,7 @@ const mockCategory = [
|
||||
updatedAt: 1649665563410,
|
||||
updatedBy: 'admin',
|
||||
provider: 'user',
|
||||
owners: [],
|
||||
href: 'http://localhost:8585/api/v1/tags/PII',
|
||||
usageCount: 0,
|
||||
children: [
|
||||
@ -283,6 +285,69 @@ jest.mock('../../components/common/EntityDescription/DescriptionV1', () => {
|
||||
return jest.fn().mockReturnValue(<p>DescriptionComponent</p>);
|
||||
});
|
||||
|
||||
jest.mock('../../components/DataAssets/OwnerLabelV2/OwnerLabelV2', () => ({
|
||||
OwnerLabelV2: jest.fn().mockImplementation(() => <div>OwnerLabelV2</div>),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'../../components/Customization/GenericProvider/GenericProvider',
|
||||
() => ({
|
||||
useGenericContext: jest.fn().mockReturnValue({
|
||||
data: {
|
||||
id: '93285c04-d8b6-4833-997e-56dc5f973427',
|
||||
name: 'PersonalData',
|
||||
description: 'description',
|
||||
version: 0.1,
|
||||
updatedAt: 1649665563400,
|
||||
updatedBy: 'admin',
|
||||
owners: [],
|
||||
href: 'http://localhost:8585/api/v1/tags/PersonalData',
|
||||
usageCount: 0,
|
||||
children: [
|
||||
{
|
||||
id: '8a218558-7b8f-446f-ace7-29b031c856b3',
|
||||
name: 'Personal',
|
||||
fullyQualifiedName: 'PersonalData.Personal',
|
||||
description:
|
||||
'Data that can be used to directly or indirectly identify a person.',
|
||||
version: 0.1,
|
||||
updatedAt: 1649665563400,
|
||||
updatedBy: 'admin',
|
||||
href: 'http://localhost:8585/api/v1/tags/PersonalData/Personal',
|
||||
usageCount: 0,
|
||||
deprecated: false,
|
||||
deleted: false,
|
||||
associatedTags: [],
|
||||
},
|
||||
{
|
||||
id: '4a2d7e47-9129-4cfe-91e8-e4f4df15f41d',
|
||||
name: 'SpecialCategory',
|
||||
fullyQualifiedName: 'PersonalData.SpecialCategory',
|
||||
description: 'description',
|
||||
version: 0.1,
|
||||
updatedAt: 1649665563400,
|
||||
updatedBy: 'admin',
|
||||
href: 'http://localhost:8585/api/v1/tags/PersonalData/SpecialCategory',
|
||||
usageCount: 0,
|
||||
deprecated: false,
|
||||
deleted: false,
|
||||
associatedTags: [],
|
||||
},
|
||||
],
|
||||
deleted: false,
|
||||
},
|
||||
onUpdate: jest.fn(),
|
||||
filterWidgets: jest.fn(),
|
||||
}),
|
||||
GenericProvider: jest.fn().mockImplementation(({ children }) => children),
|
||||
_esModule: true,
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock('../../utils/TableColumn.util', () => ({
|
||||
ownerTableObject: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
describe('Test TagsPage page', () => {
|
||||
it('Component should render', async () => {
|
||||
await act(async () => {
|
||||
|
@ -90,7 +90,6 @@ const TagsPage = () => {
|
||||
const [editTag, setEditTag] = useState<Tag>();
|
||||
const [error, setError] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isUpdateLoading, setIsUpdateLoading] = useState<boolean>(false);
|
||||
const classificationDetailsRef = useRef<ClassificationDetailsRef>(null);
|
||||
|
||||
const [deleteTags, setDeleteTags] = useState<DeleteTagsType>({
|
||||
@ -142,7 +141,11 @@ const TagsPage = () => {
|
||||
|
||||
try {
|
||||
const response = await getAllClassifications({
|
||||
fields: TabSpecificField.TERM_COUNT,
|
||||
fields: [
|
||||
TabSpecificField.TERM_COUNT,
|
||||
TabSpecificField.OWNERS,
|
||||
TabSpecificField.DOMAIN,
|
||||
],
|
||||
limit: 1000,
|
||||
});
|
||||
setClassifications(response.data);
|
||||
@ -170,7 +173,12 @@ const TagsPage = () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const currentClassification = await getClassificationByName(fqn, {
|
||||
fields: [TabSpecificField.USAGE_COUNT, TabSpecificField.TERM_COUNT],
|
||||
fields: [
|
||||
TabSpecificField.OWNERS,
|
||||
TabSpecificField.USAGE_COUNT,
|
||||
TabSpecificField.TERM_COUNT,
|
||||
TabSpecificField.DOMAIN,
|
||||
],
|
||||
});
|
||||
if (currentClassification) {
|
||||
setClassifications((prevClassifications) =>
|
||||
@ -200,7 +208,7 @@ const TagsPage = () => {
|
||||
);
|
||||
showErrorToast(errMsg);
|
||||
setError(errMsg);
|
||||
setCurrentClassification({ name: fqn, description: '' });
|
||||
setCurrentClassification(undefined);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
@ -318,8 +326,6 @@ const TagsPage = () => {
|
||||
const handleUpdateClassification = useCallback(
|
||||
async (updatedClassification: Classification) => {
|
||||
if (!isUndefined(currentClassification)) {
|
||||
setIsUpdateLoading(true);
|
||||
|
||||
const patchData = compare(currentClassification, updatedClassification);
|
||||
try {
|
||||
const response = await patchClassification(
|
||||
@ -370,8 +376,6 @@ const TagsPage = () => {
|
||||
})
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsUpdateLoading(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -716,23 +720,19 @@ const TagsPage = () => {
|
||||
secondPanel={{
|
||||
children: (
|
||||
<>
|
||||
{isUpdateLoading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<ClassificationDetails
|
||||
classificationPermissions={classificationPermissions}
|
||||
currentClassification={currentClassification}
|
||||
deleteTags={deleteTags}
|
||||
disableEditButton={disableEditButton}
|
||||
handleActionDeleteTag={handleActionDeleteTag}
|
||||
handleAddNewTagClick={handleAddNewTagClick}
|
||||
handleAfterDeleteAction={handleAfterDeleteAction}
|
||||
handleEditTagClick={handleEditTagClick}
|
||||
handleUpdateClassification={handleUpdateClassification}
|
||||
isAddingTag={isAddingTag}
|
||||
ref={classificationDetailsRef}
|
||||
/>
|
||||
)}
|
||||
<ClassificationDetails
|
||||
classificationPermissions={classificationPermissions}
|
||||
currentClassification={currentClassification}
|
||||
deleteTags={deleteTags}
|
||||
disableEditButton={disableEditButton}
|
||||
handleActionDeleteTag={handleActionDeleteTag}
|
||||
handleAddNewTagClick={handleAddNewTagClick}
|
||||
handleAfterDeleteAction={handleAfterDeleteAction}
|
||||
handleEditTagClick={handleEditTagClick}
|
||||
handleUpdateClassification={handleUpdateClassification}
|
||||
isAddingTag={isAddingTag}
|
||||
ref={classificationDetailsRef}
|
||||
/>
|
||||
|
||||
{/* Classification Form */}
|
||||
{isAddingClassification && (
|
||||
|
@ -74,7 +74,9 @@ describe('API functions should work properly', () => {
|
||||
});
|
||||
|
||||
it('createClassification function should work properly', async () => {
|
||||
const mockPostData = { name: 'testCategory' } as Classification;
|
||||
const mockPostData = {
|
||||
name: 'testCategory',
|
||||
} as Classification;
|
||||
const result = await createClassification({
|
||||
...mockPostData,
|
||||
domain: undefined,
|
||||
@ -87,7 +89,7 @@ describe('API functions should work properly', () => {
|
||||
});
|
||||
|
||||
it('createTag function should work properly', async () => {
|
||||
const mockPostData = { name: 'newTag' } as Classification;
|
||||
const mockPostData = { name: 'newTag', id: 'tagId' } as Classification;
|
||||
const result = await createTag({ ...mockPostData, domain: undefined });
|
||||
|
||||
expect(result).toEqual({
|
||||
@ -100,6 +102,7 @@ describe('API functions should work properly', () => {
|
||||
const mockUpdateData = {
|
||||
name: 'tagName',
|
||||
description: 'newDescription',
|
||||
id: 'tagId',
|
||||
};
|
||||
const result = await updateTag(mockUpdateData);
|
||||
|
||||
|
@ -43,6 +43,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
.card-padding-xss {
|
||||
> .ant-card-body {
|
||||
padding: @padding-xss;
|
||||
}
|
||||
}
|
||||
|
||||
.card-padding-xs {
|
||||
> .ant-card-body {
|
||||
padding: @padding-xs;
|
||||
}
|
||||
}
|
||||
|
||||
.card-padding-sm {
|
||||
> .ant-card-body {
|
||||
padding: @padding-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.card-padding-md {
|
||||
> .ant-card-body {
|
||||
padding: @padding-md;
|
||||
}
|
||||
}
|
||||
|
||||
.card-padding-lg {
|
||||
> .ant-card-body {
|
||||
padding: @padding-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.card-padding-y-0 {
|
||||
.ant-card-body {
|
||||
padding-top: 0;
|
||||
|
@ -65,7 +65,12 @@ import {
|
||||
patchStoredProceduresDetails,
|
||||
} from '../../rest/storedProceduresAPI';
|
||||
import { getTableDetailsByFQN, patchTableDetails } from '../../rest/tableAPI';
|
||||
import { getTagByFqn, patchTag } from '../../rest/tagAPI';
|
||||
import {
|
||||
getClassificationByName,
|
||||
getTagByFqn,
|
||||
patchClassification,
|
||||
patchTag,
|
||||
} from '../../rest/tagAPI';
|
||||
import { getTeamByName, patchTeamDetail } from '../../rest/teamsAPI';
|
||||
import { getTopicByFqn, patchTopicDetails } from '../../rest/topicsAPI';
|
||||
import { getUserByName, updateUserDetail } from '../../rest/userAPI';
|
||||
@ -102,6 +107,8 @@ export const getAPIfromSource = (
|
||||
return patchGlossaries;
|
||||
case EntityType.TAG:
|
||||
return patchTag;
|
||||
case EntityType.CLASSIFICATION:
|
||||
return patchClassification;
|
||||
case EntityType.DATABASE_SCHEMA:
|
||||
return patchDatabaseSchemaDetails;
|
||||
case EntityType.DATABASE:
|
||||
@ -161,6 +168,8 @@ export const getEntityAPIfromSource = (
|
||||
return getGlossaryTermByFQN;
|
||||
case EntityType.GLOSSARY:
|
||||
return getGlossariesByName;
|
||||
case EntityType.CLASSIFICATION:
|
||||
return getClassificationByName;
|
||||
case EntityType.TAG:
|
||||
return getTagByFqn;
|
||||
case EntityType.DATABASE_SCHEMA:
|
||||
|
@ -0,0 +1,363 @@
|
||||
/*
|
||||
* Copyright 2025 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 { EntityField } from '../constants/Feeds.constants';
|
||||
import { ProviderType } from '../generated/entity/bot';
|
||||
import { Classification } from '../generated/entity/classification/classification';
|
||||
import { ChangeDescription } from '../generated/entity/type';
|
||||
import { getClassificationInfo } from './ClassificationUtils';
|
||||
import { getEntityVersionByField } from './EntityVersionUtils';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('./EntityVersionUtils', () => ({
|
||||
getEntityVersionByField: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockGetEntityVersionByField =
|
||||
getEntityVersionByField as jest.MockedFunction<
|
||||
typeof getEntityVersionByField
|
||||
>;
|
||||
|
||||
describe('ClassificationUtils', () => {
|
||||
describe('getClassificationInfo', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return default values when no classification is provided', () => {
|
||||
const result = getClassificationInfo();
|
||||
|
||||
expect(result).toEqual({
|
||||
currentVersion: '0.1',
|
||||
isClassificationDisabled: false,
|
||||
isTier: false,
|
||||
isSystemClassification: false,
|
||||
name: undefined,
|
||||
displayName: undefined,
|
||||
description: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return correct values for a regular classification', () => {
|
||||
const mockClassification: Classification = {
|
||||
id: 'test-id',
|
||||
name: 'TestClassification',
|
||||
displayName: 'Test Classification',
|
||||
description: 'Test classification description',
|
||||
version: 1.2,
|
||||
disabled: false,
|
||||
provider: ProviderType.User,
|
||||
fullyQualifiedName: 'test.classification',
|
||||
deleted: false,
|
||||
href: 'http://test.com',
|
||||
updatedAt: 1234567890,
|
||||
updatedBy: 'test-user',
|
||||
};
|
||||
|
||||
const result = getClassificationInfo(mockClassification);
|
||||
|
||||
expect(result).toEqual({
|
||||
currentVersion: 1.2,
|
||||
isClassificationDisabled: false,
|
||||
isTier: false,
|
||||
isSystemClassification: false,
|
||||
name: 'TestClassification',
|
||||
displayName: 'Test Classification',
|
||||
description: 'Test classification description',
|
||||
});
|
||||
});
|
||||
|
||||
it('should identify Tier classification correctly', () => {
|
||||
const mockTierClassification: Classification = {
|
||||
id: 'tier-id',
|
||||
name: 'Tier',
|
||||
displayName: 'Tier Classification',
|
||||
description: 'Tier classification description',
|
||||
version: 1.0,
|
||||
disabled: false,
|
||||
provider: ProviderType.System,
|
||||
fullyQualifiedName: 'tier.classification',
|
||||
deleted: false,
|
||||
href: 'http://test.com',
|
||||
updatedAt: 1234567890,
|
||||
updatedBy: 'system',
|
||||
};
|
||||
|
||||
const result = getClassificationInfo(mockTierClassification);
|
||||
|
||||
expect(result).toEqual({
|
||||
currentVersion: 1,
|
||||
isClassificationDisabled: false,
|
||||
isTier: true,
|
||||
isSystemClassification: true,
|
||||
name: 'Tier',
|
||||
displayName: 'Tier Classification',
|
||||
description: 'Tier classification description',
|
||||
});
|
||||
});
|
||||
|
||||
it('should identify system classification correctly', () => {
|
||||
const mockSystemClassification: Classification = {
|
||||
id: 'system-id',
|
||||
name: 'SystemClassification',
|
||||
displayName: 'System Classification',
|
||||
description: 'System classification description',
|
||||
version: 2,
|
||||
disabled: true,
|
||||
provider: ProviderType.System,
|
||||
fullyQualifiedName: 'system.classification',
|
||||
deleted: false,
|
||||
href: 'http://test.com',
|
||||
updatedAt: 1234567890,
|
||||
updatedBy: 'system',
|
||||
};
|
||||
|
||||
const result = getClassificationInfo(mockSystemClassification);
|
||||
|
||||
expect(result).toEqual({
|
||||
currentVersion: 2,
|
||||
isClassificationDisabled: true,
|
||||
isTier: false,
|
||||
isSystemClassification: true,
|
||||
name: 'SystemClassification',
|
||||
displayName: 'System Classification',
|
||||
description: 'System classification description',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle disabled classification', () => {
|
||||
const mockDisabledClassification: Classification = {
|
||||
id: 'disabled-id',
|
||||
name: 'DisabledClassification',
|
||||
displayName: 'Disabled Classification',
|
||||
description: 'Disabled classification description',
|
||||
version: 1.5,
|
||||
disabled: true,
|
||||
provider: ProviderType.User,
|
||||
fullyQualifiedName: 'disabled.classification',
|
||||
deleted: false,
|
||||
href: 'http://test.com',
|
||||
updatedAt: 1234567890,
|
||||
updatedBy: 'test-user',
|
||||
};
|
||||
|
||||
const result = getClassificationInfo(mockDisabledClassification);
|
||||
|
||||
expect(result).toEqual({
|
||||
currentVersion: 1.5,
|
||||
isClassificationDisabled: true,
|
||||
isTier: false,
|
||||
isSystemClassification: false,
|
||||
name: 'DisabledClassification',
|
||||
displayName: 'Disabled Classification',
|
||||
description: 'Disabled classification description',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing optional fields gracefully', () => {
|
||||
const mockMinimalClassification: Classification = {
|
||||
id: 'minimal-id',
|
||||
name: 'MinimalClassification',
|
||||
description: 'Minimal description',
|
||||
fullyQualifiedName: 'minimal.classification',
|
||||
deleted: false,
|
||||
href: 'http://test.com',
|
||||
updatedAt: 1234567890,
|
||||
updatedBy: 'test-user',
|
||||
};
|
||||
|
||||
const result = getClassificationInfo(mockMinimalClassification);
|
||||
|
||||
expect(result).toEqual({
|
||||
currentVersion: '0.1',
|
||||
isClassificationDisabled: false,
|
||||
isTier: false,
|
||||
isSystemClassification: false,
|
||||
name: 'MinimalClassification',
|
||||
displayName: undefined,
|
||||
description: 'Minimal description',
|
||||
});
|
||||
});
|
||||
|
||||
describe('Version view functionality', () => {
|
||||
const mockChangeDescription: ChangeDescription = {
|
||||
fieldsAdded: [],
|
||||
fieldsUpdated: [],
|
||||
fieldsDeleted: [],
|
||||
previousVersion: 1.0,
|
||||
};
|
||||
|
||||
const mockClassificationWithChangeDescription: Classification = {
|
||||
id: 'versioned-id',
|
||||
name: 'VersionedClassification',
|
||||
displayName: 'Versioned Classification',
|
||||
description: 'Versioned classification description',
|
||||
version: 2,
|
||||
disabled: false,
|
||||
provider: ProviderType.User,
|
||||
fullyQualifiedName: 'versioned.classification',
|
||||
deleted: false,
|
||||
href: 'http://test.com',
|
||||
updatedAt: 1234567890,
|
||||
updatedBy: 'test-user',
|
||||
changeDescription: mockChangeDescription,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetEntityVersionByField.mockImplementation((_, field, fallback) => {
|
||||
switch (field) {
|
||||
case EntityField.NAME:
|
||||
return 'VersionedName';
|
||||
case EntityField.DISPLAYNAME:
|
||||
return 'Versioned Display Name';
|
||||
case EntityField.DESCRIPTION:
|
||||
return 'Versioned description';
|
||||
default:
|
||||
return fallback || '';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should use EntityVersionUtils when isVersionView is true', () => {
|
||||
const result = getClassificationInfo(
|
||||
mockClassificationWithChangeDescription,
|
||||
true
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
currentVersion: 2,
|
||||
isClassificationDisabled: false,
|
||||
isTier: false,
|
||||
isSystemClassification: false,
|
||||
name: 'VersionedName',
|
||||
displayName: 'Versioned Display Name',
|
||||
description: 'Versioned description',
|
||||
});
|
||||
|
||||
expect(mockGetEntityVersionByField).toHaveBeenCalledTimes(3);
|
||||
expect(mockGetEntityVersionByField).toHaveBeenCalledWith(
|
||||
mockChangeDescription,
|
||||
EntityField.NAME,
|
||||
'VersionedClassification'
|
||||
);
|
||||
expect(mockGetEntityVersionByField).toHaveBeenCalledWith(
|
||||
mockChangeDescription,
|
||||
EntityField.DISPLAYNAME,
|
||||
'Versioned Classification'
|
||||
);
|
||||
expect(mockGetEntityVersionByField).toHaveBeenCalledWith(
|
||||
mockChangeDescription,
|
||||
EntityField.DESCRIPTION,
|
||||
'Versioned classification description'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle classification with no changeDescription in version view', () => {
|
||||
const classificationWithoutChangeDescription: Classification = {
|
||||
...mockClassificationWithChangeDescription,
|
||||
changeDescription: undefined,
|
||||
};
|
||||
|
||||
const result = getClassificationInfo(
|
||||
classificationWithoutChangeDescription,
|
||||
true
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
currentVersion: 2,
|
||||
isClassificationDisabled: false,
|
||||
isTier: false,
|
||||
isSystemClassification: false,
|
||||
name: 'VersionedName',
|
||||
displayName: 'Versioned Display Name',
|
||||
description: 'Versioned description',
|
||||
});
|
||||
|
||||
// Should pass empty object as changeDescription
|
||||
expect(mockGetEntityVersionByField).toHaveBeenCalledWith(
|
||||
{},
|
||||
EntityField.NAME,
|
||||
'VersionedClassification'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not use EntityVersionUtils when isVersionView is false', () => {
|
||||
const result = getClassificationInfo(
|
||||
mockClassificationWithChangeDescription,
|
||||
false
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
currentVersion: 2,
|
||||
isClassificationDisabled: false,
|
||||
isTier: false,
|
||||
isSystemClassification: false,
|
||||
name: 'VersionedClassification',
|
||||
displayName: 'Versioned Classification',
|
||||
description: 'Versioned classification description',
|
||||
});
|
||||
|
||||
expect(mockGetEntityVersionByField).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle undefined values correctly', () => {
|
||||
const result = getClassificationInfo(undefined);
|
||||
|
||||
expect(result).toEqual({
|
||||
currentVersion: '0.1',
|
||||
isClassificationDisabled: false,
|
||||
isTier: false,
|
||||
isSystemClassification: false,
|
||||
name: undefined,
|
||||
displayName: undefined,
|
||||
description: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null values correctly', () => {
|
||||
// TypeScript would normally prevent this, but testing runtime behavior
|
||||
const result = getClassificationInfo(null as any);
|
||||
|
||||
expect(result).toEqual({
|
||||
currentVersion: '0.1',
|
||||
isClassificationDisabled: false,
|
||||
isTier: false,
|
||||
isSystemClassification: false,
|
||||
name: undefined,
|
||||
displayName: undefined,
|
||||
description: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should default isVersionView to false when not provided', () => {
|
||||
const mockClassification: Classification = {
|
||||
id: 'test-id',
|
||||
name: 'TestClassification',
|
||||
description: 'Test description',
|
||||
fullyQualifiedName: 'test.classification',
|
||||
deleted: false,
|
||||
href: 'http://test.com',
|
||||
updatedAt: 1234567890,
|
||||
updatedBy: 'test-user',
|
||||
};
|
||||
|
||||
const result = getClassificationInfo(mockClassification);
|
||||
|
||||
expect(mockGetEntityVersionByField).not.toHaveBeenCalled();
|
||||
expect(result.name).toBe('TestClassification');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -21,10 +21,14 @@ import { ReactComponent as EditIcon } from '../assets/svg/edit-new.svg';
|
||||
import { ManageButtonItemLabel } from '../components/common/ManageButtonContentItem/ManageButtonContentItem.component';
|
||||
import RichTextEditorPreviewerNew from '../components/common/RichTextEditor/RichTextEditorPreviewNew';
|
||||
import { NO_DATA_PLACEHOLDER } from '../constants/constants';
|
||||
import { EntityField } from '../constants/Feeds.constants';
|
||||
import { OperationPermission } from '../context/PermissionProvider/PermissionProvider.interface';
|
||||
import { ProviderType } from '../generated/entity/bot';
|
||||
import { Classification } from '../generated/entity/classification/classification';
|
||||
import { Tag } from '../generated/entity/classification/tag';
|
||||
import { ChangeDescription } from '../generated/entity/type';
|
||||
import { DeleteTagsType } from '../pages/TagsPage/TagsPage.interface';
|
||||
import { getEntityVersionByField } from './EntityVersionUtils';
|
||||
import { getClassificationTagPath } from './RouterUtils';
|
||||
import { getDeleteIcon, getTagImageSrc } from './TagsUtils';
|
||||
|
||||
@ -96,6 +100,7 @@ export const getCommonColumns = (): ColumnsType<Tag> => [
|
||||
title: t('label.description'),
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: 300,
|
||||
render: (text: string) => (
|
||||
<>
|
||||
<div className="cursor-pointer d-flex">
|
||||
@ -238,3 +243,37 @@ export const getClassificationExtraDropdownContent = (
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
export const getClassificationInfo = (
|
||||
currentClassification?: Classification,
|
||||
isVersionView = false
|
||||
) => {
|
||||
return {
|
||||
currentVersion: currentClassification?.version ?? '0.1',
|
||||
isClassificationDisabled: currentClassification?.disabled ?? false,
|
||||
isTier: currentClassification?.name === 'Tier',
|
||||
isSystemClassification:
|
||||
currentClassification?.provider === ProviderType.System,
|
||||
name: isVersionView
|
||||
? getEntityVersionByField(
|
||||
currentClassification?.changeDescription ?? ({} as ChangeDescription),
|
||||
EntityField.NAME,
|
||||
currentClassification?.name
|
||||
)
|
||||
: currentClassification?.name,
|
||||
displayName: isVersionView
|
||||
? getEntityVersionByField(
|
||||
currentClassification?.changeDescription ?? ({} as ChangeDescription),
|
||||
EntityField.DISPLAYNAME,
|
||||
currentClassification?.displayName
|
||||
)
|
||||
: currentClassification?.displayName,
|
||||
description: isVersionView
|
||||
? getEntityVersionByField(
|
||||
currentClassification?.changeDescription ?? ({} as ChangeDescription),
|
||||
EntityField.DESCRIPTION,
|
||||
currentClassification?.description
|
||||
)
|
||||
: currentClassification?.description,
|
||||
};
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user