fix #14292: Tags Page UI issues (#14327)

* Fixed modal css & text styling

* fix unnecessary api calls

* address comments

* added locale files

* change widget message & fix unnecessary api calls

* address comments

* fix tags page import

* fix failing cyp test

---------

Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
Harsh Vador 2023-12-19 15:25:18 +05:30 committed by GitHub
parent d79654e271
commit 824307dc9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 561 additions and 510 deletions

View File

@ -50,7 +50,7 @@ const revokeToken = () => {
// Verify the revoke text
cy.get('[data-testid="body-text"]').should(
'contain',
'Are you sure you want to revoke access for JWT token?'
'Are you sure you want to revoke access for JWT Token?'
);
interceptURL('PUT', `/api/v1/users/revokeToken`, 'revokeToken');
// Click on confirm button

View File

@ -0,0 +1,38 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Classification } from '../../generated/entity/classification/classification';
import { Tag } from '../../generated/entity/classification/tag';
import { DeleteTagsType } from '../../pages/TagsPage/TagsPage.interface';
import { OperationPermission } from '../PermissionProvider/PermissionProvider.interface';
export interface ClassificationDetailsProps {
classificationPermissions: OperationPermission;
isVersionView?: boolean;
currentClassification?: Classification;
deleteTags?: DeleteTagsType;
isEditClassification?: boolean;
isAddingTag?: boolean;
disableEditButton?: boolean;
handleAfterDeleteAction?: () => void;
handleEditTagClick?: (selectedTag: Tag) => void;
handleActionDeleteTag?: (record: Tag) => void;
handleAddNewTagClick?: () => void;
handleEditDescriptionClick?: () => void;
handleCancelEditDescription?: () => void;
handleUpdateClassification?: (
updatedClassification: Classification
) => Promise<void>;
}
export interface ClassificationDetailsRef {
refreshClassificationTags: () => void;
}

View File

@ -17,7 +17,14 @@ import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { capitalize, isUndefined, toString } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom';
import { ReactComponent as IconTag } from '../../assets/svg/classification.svg';
@ -30,23 +37,16 @@ import RichTextEditorPreviewer from '../../components/common/RichTextEditor/Rich
import Table from '../../components/common/Table/Table';
import EntityHeaderTitle from '../../components/Entity/EntityHeaderTitle/EntityHeaderTitle.component';
import { usePermissionProvider } from '../../components/PermissionProvider/PermissionProvider';
import {
OperationPermission,
ResourceEntity,
} from '../../components/PermissionProvider/PermissionProvider.interface';
import { ResourceEntity } from '../../components/PermissionProvider/PermissionProvider.interface';
import { DE_ACTIVE_COLOR } from '../../constants/constants';
import { EntityField } from '../../constants/Feeds.constants';
import { EntityType } from '../../enums/entity.enum';
import { ProviderType } from '../../generated/api/classification/createClassification';
import {
ChangeDescription,
Classification,
} from '../../generated/entity/classification/classification';
import { ChangeDescription } 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';
import { usePaging } from '../../hooks/paging/usePaging';
import { DeleteTagsType } from '../../pages/TagsPage/TagsPage.interface';
import { getTags } from '../../rest/tagAPI';
import {
getClassificationExtraDropdownContent,
@ -67,275 +67,272 @@ import { showErrorToast } from '../../utils/ToastUtils';
import ManageButton from '../common/EntityPageInfos/ManageButton/ManageButton';
import NextPrevious from '../common/NextPrevious/NextPrevious';
import { NextPreviousProps } from '../common/NextPrevious/NextPrevious.interface';
import { ClassificationDetailsProps } from './ClassificationDetails.interface';
export interface ClassificationDetailsProps {
classificationPermissions: OperationPermission;
isVersionView?: boolean;
currentClassification?: Classification;
deleteTags?: DeleteTagsType;
isEditClassification?: boolean;
isAddingTag?: boolean;
disableEditButton?: boolean;
handleAfterDeleteAction?: () => void;
handleEditTagClick?: (selectedTag: Tag) => void;
handleActionDeleteTag?: (record: Tag) => void;
handleAddNewTagClick?: () => void;
handleEditDescriptionClick?: () => void;
handleCancelEditDescription?: () => void;
handleUpdateClassification?: (
updatedClassification: Classification
) => Promise<void>;
}
const ClassificationDetails = forwardRef(
(
{
currentClassification,
handleAfterDeleteAction,
isEditClassification,
classificationPermissions,
handleUpdateClassification,
handleEditTagClick,
deleteTags,
isAddingTag,
handleActionDeleteTag,
handleAddNewTagClick,
handleEditDescriptionClick,
handleCancelEditDescription,
disableEditButton,
function ClassificationDetails({
currentClassification,
handleAfterDeleteAction,
isEditClassification,
classificationPermissions,
handleUpdateClassification,
handleEditTagClick,
deleteTags,
isAddingTag,
handleActionDeleteTag,
handleAddNewTagClick,
handleEditDescriptionClick,
handleCancelEditDescription,
disableEditButton,
isVersionView = false,
}: Readonly<ClassificationDetailsProps>) {
const { permissions } = usePermissionProvider();
const { t } = useTranslation();
const { fqn: tagCategoryName } = useParams<{ fqn: string }>();
const history = useHistory();
const [tags, setTags] = useState<Tag[]>([]);
const [isTagsLoading, setIsTagsLoading] = useState(false);
const {
currentPage,
paging,
pageSize,
handlePageChange,
handlePageSizeChange,
handlePagingChange,
showPagination,
} = usePaging();
const fetchClassificationChildren = async (
currentClassificationName: string,
paging?: Partial<Paging>
isVersionView = false,
}: Readonly<ClassificationDetailsProps>,
ref
) => {
setIsTagsLoading(true);
setTags([]);
try {
const { data, paging: tagPaging } = await getTags({
arrQueryFields: ['usageCount'],
parent: currentClassificationName,
after: paging?.after,
before: paging?.before,
limit: pageSize,
});
setTags(data);
handlePagingChange(tagPaging);
} catch (error) {
const errMsg = getErrorText(
error as AxiosError,
t('server.entity-fetch-error', { entity: t('label.tag-plural') })
);
showErrorToast(errMsg);
const { permissions } = usePermissionProvider();
const { t } = useTranslation();
const { fqn: tagCategoryName } = useParams<{ fqn: string }>();
const history = useHistory();
const [tags, setTags] = useState<Tag[]>([]);
const [isTagsLoading, setIsTagsLoading] = useState(false);
const {
currentPage,
paging,
pageSize,
handlePageChange,
handlePageSizeChange,
handlePagingChange,
showPagination,
} = usePaging();
const fetchClassificationChildren = async (
currentClassificationName: string,
paging?: Partial<Paging>
) => {
setIsTagsLoading(true);
setTags([]);
} finally {
setIsTagsLoading(false);
}
};
const handleTagsPageChange: NextPreviousProps['pagingHandler'] = ({
currentPage,
cursorType,
}) => {
if (cursorType) {
fetchClassificationChildren(
currentClassification?.fullyQualifiedName ?? '',
{
[cursorType]: paging[cursorType],
}
);
}
handlePageChange(currentPage);
};
const currentVersion = useMemo(
() => currentClassification?.version ?? '0.1',
[currentClassification]
);
const changeDescription = useMemo(
() => currentClassification?.changeDescription ?? ({} as ChangeDescription),
[currentClassification]
);
const versionHandler = useCallback(() => {
isVersionView
? history.push(getClassificationDetailsPath(tagCategoryName))
: history.push(
getClassificationVersionsPath(
tagCategoryName,
toString(currentVersion)
)
try {
const { data, paging: tagPaging } = await getTags({
arrQueryFields: ['usageCount'],
parent: currentClassificationName,
after: paging?.after,
before: paging?.before,
limit: pageSize,
});
setTags(data);
handlePagingChange(tagPaging);
} catch (error) {
const errMsg = getErrorText(
error as AxiosError,
t('server.entity-fetch-error', { entity: t('label.tag-plural') })
);
}, [currentVersion, tagCategoryName]);
showErrorToast(errMsg);
setTags([]);
} finally {
setIsTagsLoading(false);
}
};
const isTier = useMemo(
() => currentClassification?.name === 'Tier',
[currentClassification]
);
const handleTagsPageChange: NextPreviousProps['pagingHandler'] = ({
currentPage,
cursorType,
}) => {
if (cursorType) {
fetchClassificationChildren(
currentClassification?.fullyQualifiedName ?? '',
{
[cursorType]: paging[cursorType],
}
);
}
handlePageChange(currentPage);
};
const createTagPermission = useMemo(
() =>
checkPermission(Operation.Create, ResourceEntity.TAG, permissions) ||
classificationPermissions.EditAll,
[permissions, classificationPermissions]
);
const currentVersion = useMemo(
() => currentClassification?.version ?? '0.1',
[currentClassification]
);
const editClassificationPermission = useMemo(
() => classificationPermissions.EditAll,
[classificationPermissions]
);
const changeDescription = useMemo(
() =>
currentClassification?.changeDescription ?? ({} as ChangeDescription),
[currentClassification]
);
const isClassificationDisabled = useMemo(
() => currentClassification?.disabled ?? false,
[currentClassification?.disabled]
);
const versionHandler = useCallback(() => {
isVersionView
? history.push(getClassificationDetailsPath(tagCategoryName))
: history.push(
getClassificationVersionsPath(
tagCategoryName,
toString(currentVersion)
)
);
}, [currentVersion, tagCategoryName]);
const handleUpdateDisplayName = async (data: {
name: string;
displayName: string;
}) => {
if (
!isUndefined(currentClassification) &&
!isUndefined(handleUpdateClassification)
) {
return handleUpdateClassification({
...currentClassification,
...data,
});
}
};
const isTier = useMemo(
() => currentClassification?.name === 'Tier',
[currentClassification]
);
const handleUpdateDescription = async (updatedHTML: string) => {
if (
!isUndefined(currentClassification) &&
!isUndefined(handleUpdateClassification)
) {
handleUpdateClassification({
...currentClassification,
description: updatedHTML,
});
}
};
const createTagPermission = useMemo(
() =>
checkPermission(Operation.Create, ResourceEntity.TAG, permissions) ||
classificationPermissions.EditAll,
[permissions, classificationPermissions]
);
const handleEnableDisableClassificationClick = useCallback(() => {
if (
!isUndefined(currentClassification) &&
!isUndefined(handleUpdateClassification)
) {
handleUpdateClassification({
...currentClassification,
disabled: !isClassificationDisabled,
});
}
}, [
currentClassification,
handleUpdateClassification,
isClassificationDisabled,
]);
const editClassificationPermission = useMemo(
() => classificationPermissions.EditAll,
[classificationPermissions]
);
const handleUpdateMutuallyExclusive = async (value: boolean) => {
if (
!isUndefined(currentClassification) &&
!isUndefined(handleUpdateClassification)
) {
handleUpdateClassification({
...currentClassification,
mutuallyExclusive: value,
});
}
};
const isClassificationDisabled = useMemo(
() => currentClassification?.disabled ?? false,
[currentClassification?.disabled]
);
const editDescriptionPermission = useMemo(
() =>
!isVersionView &&
!isClassificationDisabled &&
(classificationPermissions.EditAll ||
classificationPermissions.EditDescription),
[classificationPermissions, isVersionView]
);
const handleUpdateDisplayName = async (data: {
name: string;
displayName: string;
}) => {
if (
!isUndefined(currentClassification) &&
!isUndefined(handleUpdateClassification)
) {
return handleUpdateClassification({
...currentClassification,
...data,
});
}
};
const isSystemClassification = useMemo(
() => currentClassification?.provider === ProviderType.System,
[currentClassification]
);
const handleUpdateDescription = async (updatedHTML: string) => {
if (
!isUndefined(currentClassification) &&
!isUndefined(handleUpdateClassification)
) {
handleUpdateClassification({
...currentClassification,
description: updatedHTML,
});
}
};
const headerBadge = useMemo(
() =>
isSystemClassification ? (
<AppBadge
icon={<LockIcon height={12} />}
label={capitalize(currentClassification?.provider)}
/>
) : null,
[isSystemClassification, currentClassification]
);
const handleEnableDisableClassificationClick = useCallback(() => {
if (
!isUndefined(currentClassification) &&
!isUndefined(handleUpdateClassification)
) {
handleUpdateClassification({
...currentClassification,
disabled: !isClassificationDisabled,
});
}
}, [
currentClassification,
handleUpdateClassification,
isClassificationDisabled,
]);
const createPermission = useMemo(
() =>
!isVersionView &&
(createTagPermission || classificationPermissions.EditAll),
[classificationPermissions, createTagPermission, isVersionView]
);
const handleUpdateMutuallyExclusive = async (value: boolean) => {
if (
!isUndefined(currentClassification) &&
!isUndefined(handleUpdateClassification)
) {
handleUpdateClassification({
...currentClassification,
mutuallyExclusive: value,
});
}
};
const deletePermission = useMemo(
() => classificationPermissions.Delete && !isSystemClassification,
[classificationPermissions, isSystemClassification]
);
const editDescriptionPermission = useMemo(
() =>
!isVersionView &&
!isClassificationDisabled &&
(classificationPermissions.EditAll ||
classificationPermissions.EditDescription),
[classificationPermissions, isVersionView]
);
const editDisplayNamePermission = useMemo(
() =>
classificationPermissions.EditAll ||
classificationPermissions.EditDisplayName,
[classificationPermissions]
);
const isSystemClassification = useMemo(
() => currentClassification?.provider === ProviderType.System,
[currentClassification]
);
const showDisableOption = useMemo(
() => !isTier && isSystemClassification && editClassificationPermission,
[isTier, isSystemClassification, editClassificationPermission]
);
const headerBadge = useMemo(
() =>
isSystemClassification ? (
<AppBadge
icon={<LockIcon height={12} />}
label={capitalize(currentClassification?.provider)}
/>
) : null,
[isSystemClassification, currentClassification]
);
const showManageButton = useMemo(
() =>
!isVersionView &&
(editDisplayNamePermission || deletePermission || showDisableOption),
[
editDisplayNamePermission,
deletePermission,
showDisableOption,
isVersionView,
]
);
const createPermission = useMemo(
() =>
!isVersionView &&
(createTagPermission || classificationPermissions.EditAll),
[classificationPermissions, createTagPermission, isVersionView]
);
const addTagButtonToolTip = useMemo(() => {
if (isClassificationDisabled) {
return t('message.disabled-classification-actions-message');
}
if (!createPermission) {
return t('message.no-permission-for-action');
}
const deletePermission = useMemo(
() => classificationPermissions.Delete && !isSystemClassification,
[classificationPermissions, isSystemClassification]
);
return null;
}, [createPermission, isClassificationDisabled]);
const editDisplayNamePermission = useMemo(
() =>
classificationPermissions.EditAll ||
classificationPermissions.EditDisplayName,
[classificationPermissions]
);
const tableColumn: ColumnsType<Tag> = useMemo(
() =>
getTagsTableColumn({
const showDisableOption = useMemo(
() => !isTier && isSystemClassification && editClassificationPermission,
[isTier, isSystemClassification, editClassificationPermission]
);
const showManageButton = useMemo(
() =>
!isVersionView &&
(editDisplayNamePermission || deletePermission || showDisableOption),
[
editDisplayNamePermission,
deletePermission,
showDisableOption,
isVersionView,
]
);
const addTagButtonToolTip = useMemo(() => {
if (isClassificationDisabled) {
return t('message.disabled-classification-actions-message');
}
if (!createPermission) {
return t('message.no-permission-for-action');
}
return null;
}, [createPermission, isClassificationDisabled]);
const tableColumn: ColumnsType<Tag> = useMemo(
() =>
getTagsTableColumn({
isClassificationDisabled,
classificationPermissions,
deleteTags,
disableEditButton,
handleEditTagClick,
handleActionDeleteTag,
isVersionView,
}),
[
isClassificationDisabled,
classificationPermissions,
deleteTags,
@ -343,236 +340,227 @@ function ClassificationDetails({
handleEditTagClick,
handleActionDeleteTag,
isVersionView,
}),
[
isClassificationDisabled,
classificationPermissions,
deleteTags,
disableEditButton,
handleEditTagClick,
handleActionDeleteTag,
isVersionView,
]
);
]
);
const extraDropdownContent = useMemo(
() =>
getClassificationExtraDropdownContent(
showDisableOption,
const extraDropdownContent = useMemo(
() =>
getClassificationExtraDropdownContent(
showDisableOption,
isClassificationDisabled,
handleEnableDisableClassificationClick
),
[
isClassificationDisabled,
handleEnableDisableClassificationClick
),
[
isClassificationDisabled,
showDisableOption,
handleEnableDisableClassificationClick,
]
);
showDisableOption,
handleEnableDisableClassificationClick,
]
);
const name = useMemo(() => {
return isVersionView
? getEntityVersionByField(
changeDescription,
EntityField.NAME,
currentClassification?.name
)
: currentClassification?.name;
}, [currentClassification, changeDescription]);
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 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]);
const description = useMemo(() => {
return isVersionView
? getEntityVersionByField(
changeDescription,
EntityField.DESCRIPTION,
currentClassification?.description
)
: currentClassification?.description;
}, [currentClassification, changeDescription]);
const mutuallyExclusive = useMemo(() => {
return isVersionView
? getMutuallyExclusiveDiff(
changeDescription,
EntityField.MUTUALLY_EXCLUSIVE,
toString(currentClassification?.mutuallyExclusive)
)
: '';
}, [currentClassification, changeDescription]);
const mutuallyExclusive = useMemo(() => {
return isVersionView
? getMutuallyExclusiveDiff(
changeDescription,
EntityField.MUTUALLY_EXCLUSIVE,
toString(currentClassification?.mutuallyExclusive)
)
: '';
}, [currentClassification, changeDescription]);
useEffect(() => {
if (
currentClassification?.fullyQualifiedName &&
!deleteTags?.state &&
!isAddingTag
) {
fetchClassificationChildren(currentClassification.fullyQualifiedName);
}
}, [
currentClassification?.fullyQualifiedName,
pageSize,
deleteTags?.state,
isAddingTag,
]);
useEffect(() => {
if (currentClassification?.fullyQualifiedName && !isAddingTag) {
fetchClassificationChildren(currentClassification.fullyQualifiedName);
}
}, [currentClassification?.fullyQualifiedName, pageSize]);
return (
<div className="p-x-md" data-testid="tags-container">
{currentClassification && (
<Row data-testid="header" wrap={false}>
<Col flex="auto">
<EntityHeaderTitle
badge={headerBadge}
className={classNames({
'opacity-60': isClassificationDisabled,
})}
displayName={displayName}
icon={
<IconTag className="h-9" style={{ color: DE_ACTIVE_COLOR }} />
}
isDisabled={isClassificationDisabled}
name={name ?? currentClassification.name}
serviceName="classification"
/>
</Col>
useImperativeHandle(ref, () => ({
refreshClassificationTags() {
if (currentClassification?.fullyQualifiedName) {
fetchClassificationChildren(currentClassification.fullyQualifiedName);
}
},
}));
<Col className="d-flex justify-end items-start" flex="270px">
<Space>
{createPermission && (
<Tooltip title={addTagButtonToolTip}>
<Button
data-testid="add-new-tag-button"
disabled={isClassificationDisabled}
type="primary"
onClick={handleAddNewTagClick}>
{t('label.add-entity', {
entity: t('label.tag'),
})}
</Button>
</Tooltip>
)}
<ButtonGroup size="small">
<Button
className="w-16 p-0"
data-testid="version-button"
icon={<Icon component={VersionIcon} />}
onClick={versionHandler}>
<Typography.Text>{currentVersion}</Typography.Text>
</Button>
{showManageButton && (
<ManageButton
isRecursiveDelete
afterDeleteAction={handleAfterDeleteAction}
allowRename={!isSystemClassification}
allowSoftDelete={false}
canDelete={deletePermission && !isClassificationDisabled}
displayName={
currentClassification.displayName ??
currentClassification.name
}
editDisplayNamePermission={
editDisplayNamePermission && !isClassificationDisabled
}
entityFQN={currentClassification.fullyQualifiedName}
entityId={currentClassification.id}
entityName={currentClassification.name}
entityType={EntityType.CLASSIFICATION}
extraDropdownContent={extraDropdownContent}
onEditDisplayName={handleUpdateDisplayName}
/>
)}
</ButtonGroup>
</Space>
</Col>
</Row>
)}
<div className="m-b-sm m-t-xs" data-testid="description-container">
<Description
className={classNames({
'opacity-60': isClassificationDisabled,
})}
description={description}
entityName={getEntityName(currentClassification)}
hasEditAccess={editDescriptionPermission}
isEdit={isEditClassification}
onCancel={handleCancelEditDescription}
onDescriptionEdit={handleEditDescriptionClick}
onDescriptionUpdate={handleUpdateDescription}
/>
</div>
<div
className="m-b-md m-t-xs d-flex justify-end"
data-testid="mutually-exclusive-container">
<Space align="center" size="small">
<Typography.Text
className="text-grey-muted"
data-testid="mutually-exclusive-classification-label">
{t('label.mutually-exclusive')}
</Typography.Text>
{isVersionView ? (
<>
<Typography.Text>:</Typography.Text>
<RichTextEditorPreviewer
className={classNames('font-medium', {
return (
<div className="p-x-md" data-testid="tags-container">
{currentClassification && (
<Row data-testid="header" wrap={false}>
<Col flex="auto">
<EntityHeaderTitle
badge={headerBadge}
className={classNames({
'opacity-60': isClassificationDisabled,
})}
markdown={mutuallyExclusive}
displayName={displayName}
icon={
<IconTag className="h-9" style={{ color: DE_ACTIVE_COLOR }} />
}
isDisabled={isClassificationDisabled}
name={name ?? currentClassification.name}
serviceName="classification"
/>
</>
) : (
<Switch
checked={currentClassification?.mutuallyExclusive}
data-testid="mutually-exclusive-classification-button"
disabled={isClassificationDisabled}
onChange={handleUpdateMutuallyExclusive}
</Col>
<Col className="d-flex justify-end items-start" flex="270px">
<Space>
{createPermission && (
<Tooltip title={addTagButtonToolTip}>
<Button
data-testid="add-new-tag-button"
disabled={isClassificationDisabled}
type="primary"
onClick={handleAddNewTagClick}>
{t('label.add-entity', {
entity: t('label.tag'),
})}
</Button>
</Tooltip>
)}
<ButtonGroup size="small">
<Button
className="w-16 p-0"
data-testid="version-button"
icon={<Icon component={VersionIcon} />}
onClick={versionHandler}>
<Typography.Text>{currentVersion}</Typography.Text>
</Button>
{showManageButton && (
<ManageButton
isRecursiveDelete
afterDeleteAction={handleAfterDeleteAction}
allowRename={!isSystemClassification}
allowSoftDelete={false}
canDelete={deletePermission && !isClassificationDisabled}
displayName={
currentClassification.displayName ??
currentClassification.name
}
editDisplayNamePermission={
editDisplayNamePermission && !isClassificationDisabled
}
entityFQN={currentClassification.fullyQualifiedName}
entityId={currentClassification.id}
entityName={currentClassification.name}
entityType={EntityType.CLASSIFICATION}
extraDropdownContent={extraDropdownContent}
onEditDisplayName={handleUpdateDisplayName}
/>
)}
</ButtonGroup>
</Space>
</Col>
</Row>
)}
<div className="m-b-sm m-t-xs" data-testid="description-container">
<Description
className={classNames({
'opacity-60': isClassificationDisabled,
})}
description={description}
entityName={getEntityName(currentClassification)}
hasEditAccess={editDescriptionPermission}
isEdit={isEditClassification}
onCancel={handleCancelEditDescription}
onDescriptionEdit={handleEditDescriptionClick}
onDescriptionUpdate={handleUpdateDescription}
/>
</div>
<div
className="m-b-md m-t-xs d-flex justify-end"
data-testid="mutually-exclusive-container">
<Space align="center" size="small">
<Typography.Text
className="text-grey-muted"
data-testid="mutually-exclusive-classification-label">
{t('label.mutually-exclusive')}
</Typography.Text>
{isVersionView ? (
<>
<Typography.Text>:</Typography.Text>
<RichTextEditorPreviewer
className={classNames('font-medium', {
'opacity-60': isClassificationDisabled,
})}
markdown={mutuallyExclusive}
/>
</>
) : (
<Switch
checked={currentClassification?.mutuallyExclusive}
data-testid="mutually-exclusive-classification-button"
disabled={isClassificationDisabled}
onChange={handleUpdateMutuallyExclusive}
/>
)}
</Space>
</div>
<Space className="w-full m-b-md" direction="vertical" size="large">
<Table
bordered
className={classNames({
'opacity-60': isClassificationDisabled,
})}
columns={tableColumn}
data-testid="table"
dataSource={tags}
loading={isTagsLoading}
locale={{
emptyText: <ErrorPlaceHolder className="m-y-md" />,
}}
pagination={false}
rowClassName={(record) => (record.disabled ? 'opacity-60' : '')}
rowKey="id"
size="small"
/>
{showPagination && !isTagsLoading && (
<NextPrevious
currentPage={currentPage}
pageSize={pageSize}
paging={paging}
pagingHandler={handleTagsPageChange}
onShowSizeChange={handlePageSizeChange}
/>
)}
</Space>
</div>
<Space className="w-full m-b-md" direction="vertical" size="large">
<Table
bordered
className={classNames({
'opacity-60': isClassificationDisabled,
})}
columns={tableColumn}
data-testid="table"
dataSource={tags}
loading={isTagsLoading}
locale={{
emptyText: <ErrorPlaceHolder className="m-y-md" />,
}}
pagination={false}
rowClassName={(record) => (record.disabled ? 'opacity-60' : '')}
rowKey="id"
size="small"
/>
{showPagination && !isTagsLoading && (
<NextPrevious
currentPage={currentPage}
pageSize={pageSize}
paging={paging}
pagingHandler={handleTagsPageChange}
onShowSizeChange={handlePageSizeChange}
/>
)}
</Space>
</div>
);
}
);
}
);
export default ClassificationDetails;

View File

@ -19,7 +19,7 @@ export interface EntityDeleteModalProp extends HTMLAttributes<HTMLDivElement> {
entityName: string;
entityType: string;
loadingState: string;
bodyText?: string;
bodyText?: string | JSX.Element;
softDelete?: boolean;
visible: boolean;
}

View File

@ -16,19 +16,18 @@ import { t } from 'i18next';
import React, { ChangeEvent, useEffect, useMemo, useState } from 'react';
import { Trans } from 'react-i18next';
import { LOADING_STATE } from '../../../enums/common.enum';
import { getTitleCase } from '../../../utils/EntityUtils';
import { Transi18next } from '../../../utils/CommonUtils';
import { EntityDeleteModalProp } from './EntityDeleteModal.interface';
const EntityDeleteModal = ({
loadingState = 'initial',
className,
entityName,
entityType,
onCancel,
onConfirm,
bodyText,
softDelete = false,
visible,
bodyText,
}: EntityDeleteModalProp) => {
const [name, setName] = useState('');
@ -92,12 +91,19 @@ const EntityDeleteModal = ({
}
width={600}>
<div data-testid="body-text">
<Typography className="mb-2">
{bodyText ||
t('message.delete-entity-permanently', {
entityType: getTitleCase(entityType),
})}
</Typography>
<div className="mb-2">
{bodyText || (
<Transi18next
i18nKey="message.permanently-delete-metadata"
renderElement={
<span data-testid="entityName" style={{ fontWeight: 500 }} />
}
values={{
entityName: entityName,
}}
/>
)}
</div>
<Typography className="mb-2">
<Trans
i18nKey="label.type-to-confirm"

View File

@ -90,9 +90,12 @@ const DeleteWidgetModal = ({
},
{
title: `${t('label.permanently-delete')} ${entityType}${entityName}`,
description: `${
deleteMessage || getDeleteMessage(entityName, entityType)
} ${hardDeleteMessagePostFix}`,
description: (
<>
{deleteMessage ?? getDeleteMessage(entityName, entityType)}
{hardDeleteMessagePostFix}
</>
),
type: DeleteType.HARD_DELETE,
isAllowed: true,
},

View File

@ -330,7 +330,7 @@ li.ProseMirror-selectednode:after {
}
.toastui-editor-md-tab-container .toastui-editor-tabs {
margin-left: 15px;
margin-left: 10px;
height: 100%;
}
@ -365,7 +365,6 @@ li.ProseMirror-selectednode:after {
.toastui-editor-defaultUI-toolbar {
display: -ms-flexbox;
display: flex;
padding: 0 25px;
height: 45px;
background-color: #f7f9fc;
border-bottom: 1px solid #ebedf2;

View File

@ -1519,7 +1519,7 @@
"password-error-message": "Das Passwort muss mindestens 8 und maximal 16 Zeichen lang sein und mindestens einen Großbuchstaben (A-Z), einen Kleinbuchstaben (a-z), eine Zahl und ein Sonderzeichen (z. B. !, %, @ oder #) enthalten.",
"password-pattern-error": "Das Passwort muss mindestens 8 und maximal 16 Zeichen lang sein und mindestens einen Sonderbuchstaben, einen Großbuchstaben und einen Kleinbuchstaben enthalten.",
"path-of-the-dbt-files-stored": "Pfad zum Ordner, in dem die dbt-Dateien gespeichert sind",
"permanently-delete-metadata": "Das dauerhafte Löschen dieses {{entityName}} entfernt seine Metadaten dauerhaft aus OpenMetadata.",
"permanently-delete-metadata": "Das dauerhafte Löschen dieses <0>{{entityName}}</0> entfernt seine Metadaten dauerhaft aus OpenMetadata.",
"permanently-delete-metadata-and-dependents": "Das dauerhafte Löschen dieses {{entityName}} entfernt seine Metadaten sowie die Metadaten von {{dependents}} dauerhaft aus OpenMetadata.",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "Beschreibung der Pipeline.",

View File

@ -1519,7 +1519,7 @@
"password-error-message": "Password must be a minimum of 8 and a maximum of 16 characters long and contain at least one uppercase character (A-Z), one lowercase character (a-z), one number, and one special character (such as !, %, @, or #)",
"password-pattern-error": "Password must be of minimum 8 and maximum 16 characters, with one special , one upper, one lower case character",
"path-of-the-dbt-files-stored": "Path of the folder where the dbt files are stored",
"permanently-delete-metadata": "Permanently deleting this {{entityName}} will remove its metadata from OpenMetadata permanently.",
"permanently-delete-metadata": "Permanently deleting this <0>{{entityName}}</0> will remove its metadata from OpenMetadata permanently.",
"permanently-delete-metadata-and-dependents": "Permanently deleting this {{entityName}} will remove its metadata, as well as the metadata of {{dependents}} from OpenMetadata permanently.",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "Description of the pipeline.",

View File

@ -1519,7 +1519,7 @@
"password-error-message": "Password must be a minimum of 8 and a maximum of 16 characters long and contain at least one uppercase character (A-Z), one lowercase character (a-z), one number, and one special character (such as !, %, @, or #)",
"password-pattern-error": "La contraseña debe tener como mínimo 8 y como máximo 16 caracteres, con un caracter especial, una letra mayúscula, y una letra minúscula.",
"path-of-the-dbt-files-stored": "Ruta de la carpeta donde se almacenan los archivos dbt",
"permanently-delete-metadata": "Al eliminar permanentemente este {{entityName}}, se eliminaran sus metadatos de OpenMetadata permanentemente.",
"permanently-delete-metadata": "Al eliminar permanentemente este <0>{{entityName}}</0>, se eliminaran sus metadatos de OpenMetadata permanentemente.",
"permanently-delete-metadata-and-dependents": "Al eliminar permanentemente este {{entityName}}, se eliminaran sus metadatos, así como los metadatos de {{dependents}} de OpenMetadata permanentemente.",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "Descripción del pipeline.",

View File

@ -1519,7 +1519,7 @@
"password-error-message": "Le mot de passe doit comporter au moins 8 caractères et au plus 16 caractères et doit contenir au moins une lettre majuscule (A-Z), une lettre minuscule (a-z), un chiffre et un caractère spécial (tel que !, %, @ ou #).",
"password-pattern-error": "Le mot de passe doit comporter au moins 8 caractères et au plus 16 caractères, avec un caractère spécial, une lettre majuscule et une lettre minuscule.",
"path-of-the-dbt-files-stored": "Chemin du dossier où sont situés les fichiers dbt.",
"permanently-delete-metadata": "La suppression permanente de cette {{entityName}} supprimera ses métadonnées de façon permanente d'OpenMetadata.",
"permanently-delete-metadata": "La suppression permanente de cette <0>{{entityName}}</0> supprimera ses métadonnées de façon permanente d'OpenMetadata.",
"permanently-delete-metadata-and-dependents": "La suppression permanente de cette {{entityName}} supprimera ses métadonnées ainsi que les métadonnées de {{dependents}} de façon permanente d'OpenMetadata.",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "Description du pipeline.",

View File

@ -1519,7 +1519,7 @@
"password-error-message": "Password must be a minimum of 8 and a maximum of 16 characters long and contain at least one uppercase character (A-Z), one lowercase character (a-z), one number, and one special character (such as !, %, @, or #)",
"password-pattern-error": "パスワードは816の文字列で、1つの特殊文字、大文字1つ、小文字1つが含まれる必要があります。",
"path-of-the-dbt-files-stored": "Path of the folder where the dbt files are stored",
"permanently-delete-metadata": "Permanently deleting this {{entityName}} will remove its metadata from OpenMetadata permanently.",
"permanently-delete-metadata": "Permanently deleting this <0>{{entityName}}</0> will remove its metadata from OpenMetadata permanently.",
"permanently-delete-metadata-and-dependents": "Permanently deleting this {{entityName}} will remove its metadata, as well as the metadata of {{dependents}} from OpenMetadata permanently.",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "パイプラインの説明",

View File

@ -1519,7 +1519,7 @@
"password-error-message": "A senha deve ter no mínimo 8 e no máximo 16 caracteres e conter pelo menos uma letra maiúscula (A-Z), uma letra minúscula (a-z), um número e um caractere especial (como !, %, @ ou #)",
"password-pattern-error": "A senha deve ter no mínimo 8 e no máximo 16 caracteres, com pelo menos um caractere especial, uma letra maiúscula e uma letra minúscula",
"path-of-the-dbt-files-stored": "Caminho da pasta onde os arquivos dbt são armazenados",
"permanently-delete-metadata": "Excluir permanentemente este(a) {{entityName}} removerá seus metadados do OpenMetadata permanentemente.",
"permanently-delete-metadata": "Excluir permanentemente este(a) <0>{{entityName}}</0> removerá seus metadados do OpenMetadata permanentemente.",
"permanently-delete-metadata-and-dependents": "Excluir permanentemente este(a) {{entityName}} removerá seus metadados, bem como os metadados de {{dependents}} do OpenMetadata permanentemente.",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "Descrição do pipeline.",

View File

@ -1519,7 +1519,7 @@
"password-error-message": "Пароль должен содержать не менее 8 и не более 16 символов и содержать как минимум один символ верхнего регистра (A-Z), один символ нижнего регистра (az), одну цифру и один специальный символ (например, !, %, @ или #). )",
"password-pattern-error": "Пароль должен состоять минимум из 8 и максимум из 16 символов, включая один специальный, один верхний и один нижний регистр.",
"path-of-the-dbt-files-stored": "Путь к папке, в которой хранятся файлы dbt",
"permanently-delete-metadata": "При окончательном удалении этого объекта {{entityName}} его метаданные будут навсегда удалены из OpenMetadata.",
"permanently-delete-metadata": "При окончательном удалении этого объекта <0>{{entityName}}</0> его метаданные будут навсегда удалены из OpenMetadata.",
"permanently-delete-metadata-and-dependents": "Безвозвратное удаление этого {{entityName}} удалит его метаданные, а также метаданные {{dependers}} из OpenMetadata навсегда.",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "Описание пайплайна.",

View File

@ -1519,7 +1519,7 @@
"password-error-message": "密码必须为8到16个字符至少包括一个大写字母A-Z、一个小写字母a-z和一个特殊字符例如!, %, @, or #",
"password-pattern-error": "密码必须为8到16个字符至少包括一个特殊字符、一个大写字母、一个小写字母",
"path-of-the-dbt-files-stored": "存储 dbt 文件的文件夹路径",
"permanently-delete-metadata": "永久删除此{{entityName}}将永久从 OpenMetadata 中删除其元数据",
"permanently-delete-metadata": "永久删除此<0>{{entityName}}</0>将永久从 OpenMetadata 中删除其元数据",
"permanently-delete-metadata-and-dependents": "永久删除此{{entityName}}将永久从 OpenMetadata 中删除其元数据以及{{dependents}}的元数据",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "工作流的描述信息",

View File

@ -16,11 +16,18 @@ import { AxiosError } from 'axios';
import classNames from 'classnames';
import { compare } from 'fast-json-patch';
import { isUndefined, omit } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom';
import { ReactComponent as PlusIcon } from '../../assets/svg/plus-primary.svg';
import ClassificationDetails from '../../components/ClassificationDetails/ClassificationDetails';
import { ClassificationDetailsRef } from '../../components/ClassificationDetails/ClassificationDetails.interface';
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
import LeftPanelCard from '../../components/common/LeftPanelCard/LeftPanelCard';
import Loader from '../../components/Loader/Loader';
@ -82,6 +89,7 @@ const TagsPage = () => {
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>({
data: undefined,
@ -278,6 +286,7 @@ const TagsPage = () => {
})
);
}
classificationDetailsRef.current?.refreshClassificationTags();
} else {
showErrorToast(
t('server.delete-entity-error', {
@ -391,6 +400,7 @@ const TagsPage = () => {
return data;
});
});
classificationDetailsRef.current?.refreshClassificationTags();
} catch (error) {
if (
(error as AxiosError).response?.status === HTTP_STATUS_CODE.CONFLICT
@ -693,7 +703,6 @@ const TagsPage = () => {
if (isLoading) {
return <Loader />;
}
if (error) {
return (
<ErrorPlaceHolder>
@ -723,6 +732,7 @@ const TagsPage = () => {
handleUpdateClassification={handleUpdateClassification}
isAddingTag={isAddingTag}
isEditClassification={isEditClassification}
ref={classificationDetailsRef}
/>
)}

View File

@ -72,7 +72,7 @@ import { EntityReference } from '../generated/entity/teams/user';
import { TagLabel } from '../generated/type/tagLabel';
import { SearchSourceAlias } from '../interface/search.interface';
import { getFeedCount } from '../rest/feedsAPI';
import { getEntityFeedLink, getTitleCase } from './EntityUtils';
import { getEntityFeedLink } from './EntityUtils';
import Fqn from './Fqn';
import { history } from './HistoryUtils';
import { getSearchIndexTabPath } from './SearchIndexUtils';
@ -498,19 +498,6 @@ export const getEntityPlaceHolder = (value: string, isDeleted?: boolean) => {
}
};
export const getEntityDeleteMessage = (entity: string, dependents: string) => {
if (dependents) {
return t('message.permanently-delete-metadata-and-dependents', {
entityName: getTitleCase(entity),
dependents,
});
} else {
return t('message.permanently-delete-metadata', {
entityName: getTitleCase(entity),
});
}
};
export const replaceSpaceWith_ = (text: string) => {
return text.replace(/\s/g, '_');
};
@ -730,6 +717,26 @@ export const Transi18next = ({
</Trans>
);
export const getEntityDeleteMessage = (entity: string, dependents: string) => {
if (dependents) {
return t('message.permanently-delete-metadata-and-dependents', {
entityName: entity,
dependents,
});
} else {
return (
<Transi18next
i18nKey="message.permanently-delete-metadata"
renderElement={
<span className="font-medium" data-testid="entityName" />
}
values={{
entityName: entity,
}}
/>
);
}
};
/**
* It takes a state and an action, and returns a new state with the action merged into it
* @param {S} state - S - The current state of the reducer.