feat(ui): Glossary name modal (#10802)

* feat: initial commit glossary redesign

* chore: add localization

* fix: update glossary ui

* fix: missing localization

* feat: update glossary ui

* fix: jest tests

* fix: jest tests

* fix: update breadcrumbs

* fix: update cypress tests

* chore: remove logs

* fix: update glossary right panel

* fix: jest tests

* fix: add reviewer functionality

* feat: add entity name and entity display name rename modal

* fix: add missing localization

* fix: update cypress tests

* fix: jest tests

* fix: redesign reviewer panel

* fix: remove breadcrumb sizing
This commit is contained in:
karanh37 2023-03-29 15:32:20 +05:30 committed by GitHub
parent 0a92a897a1
commit 63edc5d5ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 329 additions and 178 deletions

View File

@ -10,20 +10,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { Button, Col, Input, Row, Space, Tooltip, Typography } from 'antd';
import { Button, Col, Row, Space, Tooltip, Typography } from 'antd';
import DescriptionV1 from 'components/common/description/DescriptionV1';
import ProfilePicture from 'components/common/ProfilePicture/ProfilePicture';
import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component';
import { TitleBreadcrumbProps } from 'components/common/title-breadcrumb/title-breadcrumb.interface';
import { UserTeamSelectableList } from 'components/common/UserTeamSelectableList/UserTeamSelectableList.component';
import EntityDisplayNameModal from 'components/Modals/EntityDisplayNameModal/EntityDisplayNameModal.component';
import EntityNameModal from 'components/Modals/EntityNameModal/EntityNameModal.component';
import { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface';
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
import { getUserPath } from 'constants/constants';
import { NO_PERMISSION_FOR_ACTION } from 'constants/HelperTextUtil';
import { Glossary } from 'generated/entity/data/glossary';
import { GlossaryTerm } from 'generated/entity/data/glossaryTerm';
import { useAfterMount } from 'hooks/useAfterMount';
import { cloneDeep } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -51,40 +51,38 @@ const GlossaryHeader = ({
}: GlossaryHeaderProps) => {
const { t } = useTranslation();
const [displayName, setDisplayName] = useState<string>();
const [isNameEditing, setIsNameEditing] = useState<boolean>(false);
const [isDisplayNameEditing, setIsDisplayNameEditing] =
useState<boolean>(false);
const [isDescriptionEditable, setIsDescriptionEditable] =
useState<boolean>(false);
const [breadcrumb, setBreadcrumb] = useState<
TitleBreadcrumbProps['titleLinks']
>([]);
const [addTermButtonWidth, setAddTermButtonWidth] = useState(
document.getElementById('add-term-button')?.offsetWidth || 0
);
const [manageButtonWidth, setManageButtonWidth] = useState(
document.getElementById('manage-button')?.offsetWidth || 0
);
const [leftPanelWidth, setLeftPanelWidth] = useState(
document.getElementById('glossary-left-panel')?.offsetWidth || 0
);
const editDisplayNamePermission = useMemo(() => {
return permissions.EditAll || permissions.EditDisplayName;
}, [permissions]);
const onDisplayNameChange = (value: string) => {
if (selectedData.displayName !== value) {
setDisplayName(value);
}
};
const onDisplayNameSave = () => {
const onDisplayNameSave = (displayName: string) => {
let updatedDetails = cloneDeep(selectedData);
updatedDetails = {
...selectedData,
displayName: displayName?.trim(),
name: displayName?.trim() || selectedData.name,
};
onUpdate(updatedDetails);
setIsDisplayNameEditing(false);
};
const onNameSave = (name: string) => {
let updatedDetails = cloneDeep(selectedData);
updatedDetails = {
...selectedData,
name: name?.trim() || selectedData.name,
};
onUpdate(updatedDetails);
@ -137,70 +135,28 @@ const GlossaryHeader = ({
}
};
useAfterMount(() => {
setLeftPanelWidth(
document.getElementById('glossary-left-panel')?.offsetWidth || 0
);
setAddTermButtonWidth(
document.getElementById('add-term-button')?.offsetWidth || 0
);
setManageButtonWidth(
document.getElementById('manage-button')?.offsetWidth || 0
);
});
useEffect(() => {
const { displayName, fullyQualifiedName, name } = selectedData;
setDisplayName(displayName);
const { fullyQualifiedName, name } = selectedData;
if (!isGlossary) {
handleBreadcrumb(fullyQualifiedName ? fullyQualifiedName : name);
}
}, [selectedData]);
return (
<Row gutter={[0, 16]}>
<Col span={24}>
<Row justify="space-between">
<Col span={12}>
{!isGlossary && (
<div
className="tw-text-link tw-text-base glossary-breadcrumb"
data-testid="category-name">
<TitleBreadcrumb
titleLinks={breadcrumb}
widthDeductions={
leftPanelWidth + addTermButtonWidth + manageButtonWidth + 20 // Additional deduction for margin on the right of leftPanel
}
/>
</div>
)}
<>
<Row gutter={[0, 16]}>
<Col span={24}>
<Row justify="space-between">
<Col span={12}>
{!isGlossary && (
<div
className="tw-text-link tw-text-base glossary-breadcrumb"
data-testid="category-name">
<TitleBreadcrumb titleLinks={breadcrumb} />
</div>
)}
{isNameEditing ? (
<Space direction="horizontal">
<Input
className="input-width"
data-testid="displayName"
name="displayName"
value={displayName}
onChange={(e) => onDisplayNameChange(e.target.value)}
/>
<Button
data-testid="cancelAssociatedTag"
icon={<CloseOutlined />}
size="small"
type="primary"
onMouseDown={() => setIsNameEditing(false)}
/>
<Button
data-testid="saveAssociatedTag"
icon={<CheckOutlined />}
size="small"
type="primary"
onMouseDown={onDisplayNameSave}
/>
</Space>
) : (
<Space direction="vertical" size={0}>
<Space>
<Typography.Text
@ -237,7 +193,9 @@ const GlossaryHeader = ({
<Tooltip
title={
editDisplayNamePermission
? t('label.edit-entity', { entity: t('label.name') })
? t('label.edit-entity', {
entity: t('label.display-name'),
})
: NO_PERMISSION_FOR_ACTION
}>
<Button
@ -248,75 +206,87 @@ const GlossaryHeader = ({
}
size="small"
type="text"
onClick={() => setIsNameEditing(true)}
onClick={() => setIsDisplayNameEditing(true)}
/>
</Tooltip>
</Space>
</Space>
)}
</Col>
<Col span={12}>
<div style={{ textAlign: 'right' }}>
<GlossaryHeaderButtons
deleteStatus="success"
isGlossary={isGlossary}
permission={permissions}
selectedData={selectedData}
onEntityDelete={onDelete}
/>
</div>
</Col>
</Row>
</Col>
<Col span={24}>
<Space className="flex-wrap" direction="horizontal">
<div className="flex items-center">
<Typography.Text className="text-grey-muted m-r-xs">
{`${t('label.owner')}:`}
</Typography.Text>
{selectedData.owner && getEntityName(selectedData.owner) ? (
<Space className="m-r-xss" size={4}>
<ProfilePicture
displayName={getEntityName(selectedData.owner)}
id={selectedData.owner?.id || ''}
name={selectedData.owner?.name || ''}
textClass="text-xs"
width="20"
</Col>
<Col span={12}>
<div style={{ textAlign: 'right' }}>
<GlossaryHeaderButtons
deleteStatus="success"
isGlossary={isGlossary}
permission={permissions}
selectedData={selectedData}
onEntityDelete={onDelete}
/>
<Link to={getUserPath(selectedData.owner.name ?? '')}>
{getEntityName(selectedData.owner)}
</Link>
</Space>
) : (
<span className="text-grey-muted">
{t('label.no-entity', {
entity: t('label.owner-lowercase'),
})}
</span>
)}
<div className="tw-relative">
<UserTeamSelectableList
hasPermission={permissions.EditOwner || permissions.EditAll}
owner={selectedData.owner}
onUpdate={handleUpdatedOwner}
/>
</div>
</Col>
</Row>
</Col>
<Col span={24}>
<Space className="flex-wrap" direction="horizontal">
<div className="flex items-center">
<Typography.Text className="text-grey-muted m-r-xs">
{`${t('label.owner')}:`}
</Typography.Text>
{selectedData.owner && getEntityName(selectedData.owner) ? (
<Space className="m-r-xss" size={4}>
<ProfilePicture
displayName={getEntityName(selectedData.owner)}
id={selectedData.owner?.id || ''}
name={selectedData.owner?.name || ''}
textClass="text-xs"
width="20"
/>
<Link to={getUserPath(selectedData.owner.name ?? '')}>
{getEntityName(selectedData.owner)}
</Link>
</Space>
) : (
<span className="text-grey-muted">
{t('label.no-entity', {
entity: t('label.owner-lowercase'),
})}
</span>
)}
<div className="tw-relative">
<UserTeamSelectableList
hasPermission={permissions.EditOwner || permissions.EditAll}
owner={selectedData.owner}
onUpdate={handleUpdatedOwner}
/>
</div>
</div>
</div>
</Space>
</Col>
<Col data-testid="updated-by-container" span={24}>
<DescriptionV1
description={selectedData?.description || ''}
entityName={selectedData?.displayName ?? selectedData?.name}
hasEditAccess={permissions.EditDescription || permissions.EditAll}
isEdit={isDescriptionEditable}
onCancel={() => setIsDescriptionEditable(false)}
onDescriptionEdit={() => setIsDescriptionEditable(true)}
onDescriptionUpdate={onDescriptionUpdate}
/>
</Col>
</Row>
</Space>
</Col>
<Col data-testid="updated-by-container" span={24}>
<DescriptionV1
description={selectedData?.description || ''}
entityName={selectedData?.displayName ?? selectedData?.name}
hasEditAccess={permissions.EditDescription || permissions.EditAll}
isEdit={isDescriptionEditable}
onCancel={() => setIsDescriptionEditable(false)}
onDescriptionEdit={() => setIsDescriptionEditable(true)}
onDescriptionUpdate={onDescriptionUpdate}
/>
</Col>
</Row>
<EntityNameModal
name={selectedData.name}
visible={isNameEditing}
onCancel={() => setIsNameEditing(false)}
onSave={onNameSave}
/>
<EntityDisplayNameModal
displayName={selectedData.displayName || ''}
visible={isDisplayNameEditing}
onCancel={() => setIsDisplayNameEditing(false)}
onSave={onDisplayNameSave}
/>
</>
);
};

View File

@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { act, fireEvent, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { Glossary } from 'generated/entity/data/glossary';
import React from 'react';
import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils';
@ -111,41 +111,6 @@ describe('GlossaryHeader component', () => {
expect(screen.getByTestId('edit-name')).toBeDisabled();
});
it('should show editing of name after clicking on edit icon', () => {
render(
<GlossaryHeader
isGlossary
permissions={{
...DEFAULT_ENTITY_PERMISSION,
EditAll: true,
EditDisplayName: true,
}}
selectedData={
{
displayName: 'glossaryTest',
reviewers: [
{ displayName: 'reviewer1' },
{ displayName: 'reviewer2' },
],
} as Glossary
}
onDelete={mockOnDelete}
onUpdate={mockOnUpdate}
/>
);
expect(screen.getByTestId('edit-name')).toBeInTheDocument();
act(() => {
fireEvent.click(screen.getByTestId('edit-name'));
});
expect(screen.getByTestId('displayName')).toBeInTheDocument();
expect(screen.getByTestId('displayName')).toHaveValue('glossaryTest');
expect(screen.getByTestId('cancelAssociatedTag')).toBeInTheDocument();
expect(screen.getByTestId('saveAssociatedTag')).toBeInTheDocument();
});
it('should render no owner if owner is not present', () => {
render(
<GlossaryHeader

View File

@ -83,6 +83,10 @@ const GlossaryHeaderButtons = ({
} else {
history.push(getAddGlossaryTermsPath(glossary));
}
} else {
history.push(
getAddGlossaryTermsPath(selectedData.fullyQualifiedName ?? '')
);
}
};

View File

@ -0,0 +1,93 @@
/*
* 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 { Button, Form, Input, Modal, Typography } from 'antd';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
interface Props {
visible: boolean;
displayName: string;
onCancel: () => void;
onSave: (displayName: string) => void;
}
const EntityDisplayNameModal: React.FC<Props> = ({
visible,
displayName,
onCancel,
onSave,
}) => {
const { t } = useTranslation();
const [form] = Form.useForm<{ displayName: string }>();
const handleSave = async (obj: { displayName: string }) => {
try {
await form.validateFields();
onSave(obj.displayName);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
form.setFieldValue('displayName', displayName);
}, [visible]);
return (
<Modal
destroyOnClose
footer={[
<Button key="cancel-btn" type="link" onClick={onCancel}>
{t('label.cancel')}
</Button>,
<Button
data-testid="save-button"
key="save-btn"
type="primary"
onClick={() => form.submit()}>
{t('label.save')}
</Button>,
]}
okText={t('label.save')}
title={
<Typography.Text strong data-testid="header">
{t('label.edit-glossary-display-name')}
</Typography.Text>
}
visible={visible}>
<Form form={form} layout="vertical" onFinish={handleSave}>
<Form.Item
extra={
<Typography.Text className="help-text p-x-xs tw-text-xs tw-text-grey-muted">
{t('message.edit-glossary-display-name-help')}
</Typography.Text>
}
initialValue={displayName}
label={`${t('label.display-name')}:`}
name="displayName"
rules={[
{
required: true,
message: `${t('label.field-required', {
field: t('label.name'),
})}`,
},
]}>
<Input placeholder={t('message.enter-display-name')} />
</Form.Item>
</Form>
</Modal>
);
};
export default EntityDisplayNameModal;

View File

@ -0,0 +1,97 @@
/*
* 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 { Button, Form, Input, Modal, Typography } from 'antd';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
interface Props {
visible: boolean;
name: string;
onCancel: () => void;
onSave: (name: string) => void;
}
const EntityNameModal: React.FC<Props> = ({
visible,
name,
onCancel,
onSave,
}) => {
const { t } = useTranslation();
const [form] = Form.useForm<{ name: string }>();
const handleSave = async (obj: { name: string }) => {
try {
await form.validateFields();
onSave(obj.name);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
form.setFieldValue('name', name);
}, [visible]);
return (
<Modal
destroyOnClose
footer={[
<Button key="cancel-btn" type="link" onClick={onCancel}>
{t('label.cancel')}
</Button>,
<Button
data-testid="save-button"
key="save-btn"
type="primary"
onClick={() => form.submit()}>
{t('label.save')}
</Button>,
]}
okText={t('label.save')}
open={visible}
title={
<Typography.Text strong data-testid="header">
{t('label.edit-glossary-name')}
</Typography.Text>
}>
<Form form={form} layout="vertical" onFinish={handleSave}>
<Form.Item
extra={
<Typography.Text className="help-text p-x-xs m-t-xs tw-text-xs tw-text-grey-muted">
{t('message.edit-glossary-name-help')}
</Typography.Text>
}
initialValue={name}
label={`${t('label.name')}:`}
name="name"
rules={[
{
required: true,
message: `${t('label.field-required', {
field: t('label.name'),
})}`,
},
]}>
<Input
placeholder={t('label.enter-entity-name', {
entity: t('label.glossary'),
})}
/>
</Form.Item>
</Form>
</Modal>
);
};
export default EntityNameModal;

View File

@ -245,6 +245,8 @@
"edit-description-for": "Edit Description for {{entityName}}",
"edit-entity": "Edit {{entity}}",
"edit-entity-name": "Edit {{entityType}}: \"{{entityName}}\"",
"edit-glossary-display-name": "Edit Glossary Display Name",
"edit-glossary-name": "Edit Glossary Name",
"edit-workflow-ingestion": "Edit {{workflow}} Ingestion",
"edited": "Edited",
"effect": "Effect",
@ -959,6 +961,8 @@
"downstream-depth-message": "Please select a value for downstream depth",
"downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).",
"drag-and-drop-files-here": "Drag & drop files here",
"edit-glossary-display-name-help": "Update Display Name",
"edit-glossary-name-help": "Changing Name will remove the existing tag and create new one with mentioned name",
"edit-service-entity-connection": "Edit {{entity}} Service Connection",
"elastic-search-message": "Ensure that your Elasticsearch indexes are up-to-date by syncing, or recreating all indexes.",
"elasticsearch-setup": "Please follow the instructions here to set up Metadata ingestion and index them into Elasticsearch.",

View File

@ -245,6 +245,8 @@
"edit-description-for": "Mettre à Jour la Description pour {{entityName}}",
"edit-entity": "Mettre à Jour {{entity}}",
"edit-entity-name": "Edit {{entityType}}: \"{{entityName}}\"",
"edit-glossary-display-name": "Edit Glossary Display Name",
"edit-glossary-name": "Edit Glossary Name",
"edit-workflow-ingestion": "Edit {{workflow}} Ingestion",
"edited": "Edited",
"effect": "Effet",
@ -959,6 +961,8 @@
"downstream-depth-message": "Please select a value for downstream depth",
"downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).",
"drag-and-drop-files-here": "Drag & drop files here",
"edit-glossary-display-name-help": "Update Display Name",
"edit-glossary-name-help": "Changing Name will remove the existing tag and create new one with mentioned name",
"edit-service-entity-connection": "Edit {{entity}} Service Connection",
"elastic-search-message": "Ensure that your Elasticsearch indexes are up-to-date by syncing, or recreating all indexes.",
"elasticsearch-setup": "Please follow the instructions here to set up Metadata ingestion and index them into Elasticsearch.",

View File

@ -245,6 +245,8 @@
"edit-description-for": "{{entityName}}の説明を編集",
"edit-entity": "{{entity}}を編集",
"edit-entity-name": "{{entityType}}: \"{{entityName}}\" を編集",
"edit-glossary-display-name": "Edit Glossary Display Name",
"edit-glossary-name": "Edit Glossary Name",
"edit-workflow-ingestion": "{{workflow}}インジェスチョンを編集",
"edited": "編集済",
"effect": "エフェクト",
@ -959,6 +961,8 @@
"downstream-depth-message": "Please select a value for downstream depth",
"downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).",
"drag-and-drop-files-here": "ここにファイルをドラッグ&ドロップ",
"edit-glossary-display-name-help": "Update Display Name",
"edit-glossary-name-help": "Changing Name will remove the existing tag and create new one with mentioned name",
"edit-service-entity-connection": "{{entity}}サービスとの接続を編集する",
"elastic-search-message": "Ensure that your Elasticsearch indexes are up-to-date by syncing, or recreating all indexes.",
"elasticsearch-setup": "Please follow the instructions here to set up Metadata ingestion and index them into Elasticsearch.",

View File

@ -245,6 +245,8 @@
"edit-description-for": "编辑描述 {{entityName}}",
"edit-entity": "编辑 {{entity}}",
"edit-entity-name": "编辑 {{entityType}}: \"{{entityName}}\"",
"edit-glossary-display-name": "Edit Glossary Display Name",
"edit-glossary-name": "Edit Glossary Name",
"edit-workflow-ingestion": "编辑 {{workflow}} 获取",
"edited": "编辑",
"effect": "Effect",
@ -959,6 +961,8 @@
"downstream-depth-message": "Please select a value for downstream depth",
"downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).",
"drag-and-drop-files-here": "Drag & drop files here",
"edit-glossary-display-name-help": "Update Display Name",
"edit-glossary-name-help": "Changing Name will remove the existing tag and create new one with mentioned name",
"edit-service-entity-connection": "Edit {{entity}} Service Connection",
"elastic-search-message": "Ensure that your Elasticsearch indexes are up-to-date by syncing, or recreating all indexes.",
"elasticsearch-setup": "Please follow the instructions here to set up Metadata ingestion and index them into Elasticsearch.",

View File

@ -180,7 +180,6 @@ const GlossaryRightPanel = ({
data-testid={`reviewer-${reviewer.displayName}`}
key={reviewer.name}>
<UserTag
bordered
id={reviewer.id || ''}
key={reviewer.name}
name={reviewer?.name || ''}

View File

@ -1298,3 +1298,10 @@ div.ant-typography-ellipsis-custom {
svg {
vertical-align: baseline;
}
/**
* Style for Antd Form Item Extra
*/
.help-text {
box-decoration-break: clone;
}