chore(ui): wait before udpate the ui for update calls (#15356)

* chore(ui): wait before udpate the ui for update calls

* fix some more cases

* fix unit tests

* fix cypress
This commit is contained in:
Chirag Madlani 2024-02-27 19:49:29 +05:30 committed by GitHub
parent da926d1f2d
commit 8c46a4e5cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 237 additions and 371 deletions

View File

@ -332,7 +332,7 @@ const DataProductsDetailsPage = ({
}
};
const onStyleSave = (data: Style) => {
const onStyleSave = async (data: Style) => {
const style: Style = {
// if color/iconURL is empty or undefined send undefined
color: data.color ? data.color : undefined,
@ -343,7 +343,7 @@ const DataProductsDetailsPage = ({
style,
};
onUpdate(updatedDetails);
await onUpdate(updatedDetails);
setIsStyleEditing(false);
};

View File

@ -16,7 +16,7 @@ export interface DataProductsDetailsPageProps {
dataProduct: DataProduct;
isVersionsView?: boolean;
onUpdate: (dataProductDetails: DataProduct) => Promise<void>;
onDelete: () => void;
onDelete: () => Promise<void>;
}
export enum DataProductTabs {

View File

@ -24,6 +24,7 @@ import { TagLabel } from '../../../../generated/type/tagLabel';
import { getEntityName } from '../../../../utils/EntityUtils';
import Description from '../../../common/EntityDescription/Description';
import Loader from '../../../common/Loader/Loader';
import { OwnerLabel } from '../../../common/OwnerLabel/OwnerLabel.component';
import ProfilePicture from '../../../common/ProfilePicture/ProfilePicture';
import { UserTeamSelectableList } from '../../../common/UserTeamSelectableList/UserTeamSelectableList.component';
import TagsInput from '../../../TagsInput/TagsInput.component';
@ -102,28 +103,7 @@ const TableQueryRightPanel = ({
</UserTeamSelectableList>
)}
</Space>
<div data-testid="owner-name-container">
{query.owner && getEntityName(query.owner) ? (
<Space className="m-r-xss" size={4}>
<ProfilePicture
displayName={getEntityName(query.owner)}
name={query.owner?.name || ''}
width="20"
/>
<Link
data-testid="owner-link"
to={getUserPath(query.owner.name ?? '')}>
{getEntityName(query.owner)}
</Link>
</Space>
) : (
<span className="text-grey-muted">
{t('label.no-entity', {
entity: t('label.owner-lowercase'),
})}
</span>
)}
</div>
<OwnerLabel hasPermission={false} owner={query.owner} />
</Space>
</Col>
<Col span={24}>

View File

@ -62,9 +62,6 @@ describe('TableQueryRightPanel component test', () => {
});
const owner = await screen.findByTestId('owner-link');
expect(
await screen.findByTestId('owner-name-container')
).toBeInTheDocument();
expect(owner).toBeInTheDocument();
expect(owner.textContent).toEqual(MOCK_QUERIES[0].owner?.displayName);
expect(

View File

@ -312,7 +312,7 @@ const DomainDetailsPage = ({
setIsNameEditing(false);
};
const onStyleSave = (data: Style) => {
const onStyleSave = async (data: Style) => {
const style: Style = {
// if color/iconURL is empty or undefined send undefined
color: data.color ? data.color : undefined,
@ -323,7 +323,7 @@ const DomainDetailsPage = ({
style,
};
onUpdate(updatedDetails);
await onUpdate(updatedDetails);
setIsStyleEditing(false);
};

View File

@ -136,7 +136,7 @@ const DocumentationTab = ({
await onUpdate(updatedData as Domain | DataProduct);
};
const handleExpertsUpdate = (data: Array<EntityReference>) => {
const handleExpertsUpdate = async (data: Array<EntityReference>) => {
if (!isEqual(data, domain.experts)) {
let updatedDomain = cloneDeep(domain);
const oldExperts = data.filter((d) => includes(domain.experts, d));
@ -152,7 +152,7 @@ const DocumentationTab = ({
...updatedDomain,
experts: [...oldExperts, ...newExperts],
};
onUpdate(updatedDomain);
await onUpdate(updatedDomain);
}
};

View File

@ -123,7 +123,7 @@ const EntityVersionTimeLine: React.FC<EntityVersionTimelineProps> = ({
const versions = useMemo(
() =>
versionList.versions.map((v, i) => {
versionList.versions?.map((v, i) => {
const currV = JSON.parse(v);
const majorVersionChecks = () => {

View File

@ -24,12 +24,10 @@ import {
getFormattedEntityData,
getSortedTagsWithHighlight,
} from '../../../../utils/EntitySummaryPanelUtils';
import {
DRAWER_NAVIGATION_OPTIONS,
getOwnerNameWithProfilePic,
} from '../../../../utils/EntityUtils';
import { DRAWER_NAVIGATION_OPTIONS } from '../../../../utils/EntityUtils';
import { bytesToSize } from '../../../../utils/StringsUtils';
import { getConfigObject } from '../../../../utils/TopicDetailsUtils';
import { OwnerLabel } from '../../../common/OwnerLabel/OwnerLabel.component';
import SummaryPanelSkeleton from '../../../common/Skeleton/SummaryPanelSkeleton/SummaryPanelSkeleton.component';
import SummaryTagsDescription from '../../../common/SummaryTagsDescription/SummaryTagsDescription.component';
import { SearchedDataProps } from '../../../SearchedData/SearchedData.interface';
@ -75,11 +73,7 @@ function TopicSummary({
const owner = entityDetails.owner;
return {
value:
getOwnerNameWithProfilePic(owner) ??
t('label.no-entity', {
entity: t('label.owner'),
}),
value: <OwnerLabel hasPermission={false} owner={owner} />,
url: getTeamAndUserDetailsPath(owner?.name ?? ''),
isLink: !isEmpty(owner?.name),
};

View File

@ -29,7 +29,7 @@ export type GlossaryDetailsProps = {
termsLoading: boolean;
updateGlossary: (value: Glossary) => Promise<void>;
updateVote?: (data: VotingDataProps) => Promise<void>;
handleGlossaryDelete: (id: string) => void;
handleGlossaryDelete: (id: string) => Promise<void>;
refreshGlossaryTerms: () => void;
onAddGlossaryTerm: (glossaryTerm: GlossaryTerm | undefined) => void;
onEditGlossaryTerm: (glossaryTerm: GlossaryTerm) => void;

View File

@ -32,6 +32,7 @@ import {
getDiffByFieldName,
getRemovedDiffElement,
} from '../../../utils/EntityVersionUtils';
import { UserTeam } from '../../common/AssigneeList/AssigneeList.interface';
import ProfilePicture from '../../common/ProfilePicture/ProfilePicture';
interface GlossaryReviewersProps {
@ -70,6 +71,7 @@ function GlossaryReviewers({
<Space className="m-r-xss" key={reviewer.id} size={4}>
<ProfilePicture
displayName={getEntityName(reviewer)}
isTeam={reviewer.type === UserTeam.Team}
name={reviewer.name ?? ''}
textClass="text-xs"
width="20"

View File

@ -207,9 +207,9 @@ const GlossaryHeader = ({
history.push(path);
};
const handleDelete = () => {
const handleDelete = async () => {
const { id } = selectedData;
onDelete(id);
await onDelete(id);
setIsDelete(false);
};
@ -227,7 +227,7 @@ const GlossaryHeader = ({
setIsNameEditing(false);
};
const onStyleSave = (data: Style) => {
const onStyleSave = async (data: Style) => {
const style: Style = {
// if color/iconURL is empty or undefined send undefined
color: data.color ? data.color : undefined,
@ -238,7 +238,7 @@ const GlossaryHeader = ({
style,
};
onUpdate(updatedDetails);
await onUpdate(updatedDetails);
setIsStyleEditing(false);
};

View File

@ -21,8 +21,8 @@ export interface GlossaryHeaderProps {
permissions: OperationPermission;
selectedData: Glossary | GlossaryTerm;
isGlossary: boolean;
onUpdate: (data: GlossaryTerm | Glossary) => void | Promise<void>;
onDelete: (id: string) => void;
onUpdate: (data: GlossaryTerm | Glossary) => Promise<void>;
onDelete: (id: string) => Promise<void>;
onAssetAdd?: () => void;
updateVote?: (data: VotingDataProps) => Promise<void>;
onAddGlossaryTerm: (glossaryTerm: GlossaryTerm | undefined) => void;

View File

@ -12,7 +12,7 @@
*/
import Icon from '@ant-design/icons/lib/components/Icon';
import { Button, Col, Form, Input, Modal, Row } from 'antd';
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as IconDelete } from '../../../assets/svg/ic-delete.svg';
import { ReactComponent as PlusIcon } from '../../../assets/svg/plus-primary.svg';
@ -22,7 +22,7 @@ interface GlossaryTermReferencesModalProps {
references: TermReference[];
isVisible: boolean;
onClose: () => void;
onSave: (values: TermReference[]) => void;
onSave: (values: TermReference[]) => Promise<void>;
}
const GlossaryTermReferencesModal = ({
@ -33,13 +33,17 @@ const GlossaryTermReferencesModal = ({
}: GlossaryTermReferencesModalProps) => {
const { t } = useTranslation();
const [form] = Form.useForm<{ references: TermReference[] }>();
const [saving, setSaving] = useState<boolean>(false);
const handleSubmit = async (obj: { references: TermReference[] }) => {
try {
setSaving(true);
await form.validateFields();
onSave(obj.references);
await onSave(obj.references);
} catch (_) {
// Nothing here
} finally {
setSaving(false);
}
};
@ -69,8 +73,9 @@ const GlossaryTermReferencesModal = ({
<Button
data-testid="save-btn"
key="save-btn"
loading={saving}
type="primary"
onClick={() => form.submit()}>
onClick={form.submit}>
{t('label.save')}
</Button>,
]}

View File

@ -26,6 +26,7 @@ import { FEED_COUNT_INITIAL_DATA } from '../../../constants/entity.constants';
import { EntityField } from '../../../constants/Feeds.constants';
import { EntityTabs, EntityType } from '../../../enums/entity.enum';
import { SearchIndex } from '../../../enums/search.enum';
import { Glossary } from '../../../generated/entity/data/glossary';
import {
GlossaryTerm,
Status,
@ -153,8 +154,8 @@ const GlossaryTermsV1 = ({
[glossaryTerm, handleGlossaryTermUpdate]
);
const onTermUpdate = async (data: GlossaryTerm) => {
await handleGlossaryTermUpdate(data);
const onTermUpdate = async (data: GlossaryTerm | Glossary) => {
await handleGlossaryTermUpdate(data as GlossaryTerm);
getEntityFeedCount();
};
@ -170,7 +171,7 @@ const GlossaryTermsV1 = ({
permissions={permissions}
selectedData={glossaryTerm}
onThreadLinkSelect={onThreadLinkSelect}
onUpdate={async (data) => await onTermUpdate(data as GlossaryTerm)}
onUpdate={onTermUpdate}
/>
),
},
@ -334,7 +335,7 @@ const GlossaryTermsV1 = ({
onAddGlossaryTerm={onAddGlossaryTerm}
onAssetAdd={() => setAssetModelVisible(true)}
onDelete={handleGlossaryTermDelete}
onUpdate={(data) => onTermUpdate(data as GlossaryTerm)}
onUpdate={onTermUpdate}
/>
</Col>

View File

@ -21,7 +21,7 @@ export interface GlossaryTermsV1Props {
glossaryTerm: GlossaryTerm;
childGlossaryTerms: GlossaryTerm[];
handleGlossaryTermUpdate: (data: GlossaryTerm) => Promise<void>;
handleGlossaryTermDelete: (id: string) => void;
handleGlossaryTermDelete: (id: string) => Promise<void>;
refreshGlossaryTerms: () => void;
onAssetClick?: (asset?: EntityDetailsObjectInterface) => void;
isSummaryPanelOpen: boolean;

View File

@ -47,7 +47,7 @@ interface GlossaryTermReferencesProps {
isVersionView?: boolean;
glossaryTerm: GlossaryTerm;
permissions: OperationPermission;
onGlossaryTermUpdate: (glossaryTerm: GlossaryTerm) => void;
onGlossaryTermUpdate: (glossaryTerm: GlossaryTerm) => Promise<void>;
}
const GlossaryTermReferences = ({
@ -74,7 +74,7 @@ const GlossaryTermReferences = ({
references: updatedRef,
};
onGlossaryTermUpdate(updatedGlossaryTerm);
await onGlossaryTermUpdate(updatedGlossaryTerm);
if (updateState) {
setReferences(updatedRef);
}
@ -85,10 +85,6 @@ const GlossaryTermReferences = ({
}
};
const onReferenceModalSave = (values: TermReference[]) => {
handleReferencesSave(values);
};
useEffect(() => {
setReferences(glossaryTerm.references ? glossaryTerm.references : []);
}, [glossaryTerm.references]);
@ -246,9 +242,7 @@ const GlossaryTermReferences = ({
onClose={() => {
setIsViewMode(true);
}}
onSave={(values: TermReference[]) => {
onReferenceModalSave(values);
}}
onSave={handleReferencesSave}
/>
</div>
);

View File

@ -38,7 +38,7 @@ interface GlossaryTermSynonymsProps {
isVersionView?: boolean;
permissions: OperationPermission;
glossaryTerm: GlossaryTerm;
onGlossaryTermUpdate: (glossaryTerm: GlossaryTerm) => void;
onGlossaryTermUpdate: (glossaryTerm: GlossaryTerm) => Promise<void>;
}
const GlossaryTermSynonyms = ({
@ -49,6 +49,7 @@ const GlossaryTermSynonyms = ({
}: GlossaryTermSynonymsProps) => {
const [isViewMode, setIsViewMode] = useState<boolean>(true);
const [synonyms, setSynonyms] = useState<string[]>([]);
const [saving, setSaving] = useState<boolean>(false);
const getSynonyms = () => (
<div className="d-flex flex-wrap">
@ -156,15 +157,16 @@ const GlossaryTermSynonyms = ({
setIsViewMode(true);
};
const handleSynonymsSave = (newSynonyms: string[]) => {
if (!isEqual(newSynonyms, glossaryTerm.synonyms)) {
const handleSynonymsSave = async () => {
if (!isEqual(synonyms, glossaryTerm.synonyms)) {
let updatedGlossaryTerm = cloneDeep(glossaryTerm);
updatedGlossaryTerm = {
...updatedGlossaryTerm,
synonyms: newSynonyms,
synonyms,
};
onGlossaryTermUpdate(updatedGlossaryTerm);
setSaving(true);
await onGlossaryTermUpdate(updatedGlossaryTerm);
setSaving(false);
}
setIsViewMode(true);
};
@ -217,9 +219,10 @@ const GlossaryTermSynonyms = ({
className="w-6 p-x-05"
data-testid="save-synonym-btn"
icon={<CheckOutlined size={12} />}
loading={saving}
size="small"
type="primary"
onClick={() => handleSynonymsSave(synonyms)}
onClick={handleSynonymsSave}
/>
</Space>

View File

@ -77,6 +77,7 @@ const RelatedTerms = ({
if (!isArray(selectedData)) {
return;
}
const newOptions = uniqWith(
options,
(arrVal, othVal) => arrVal.id === othVal.id

View File

@ -23,8 +23,8 @@ export type GlossaryV1Props = {
isGlossaryActive: boolean;
updateGlossary: (value: Glossary) => Promise<void>;
onGlossaryTermUpdate: (value: GlossaryTerm) => Promise<void>;
onGlossaryDelete: (id: string) => void | Promise<void>;
onGlossaryTermDelete: (id: string) => void | Promise<void>;
onGlossaryDelete: (id: string) => Promise<void>;
onGlossaryTermDelete: (id: string) => Promise<void>;
isVersionsView: boolean;
onAssetClick?: (asset?: EntityDetailsObjectInterface) => void;
isSummaryPanelOpen: boolean;

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { AxiosError } from 'axios';
import { noop, toString } from 'lodash';
import { toString } from 'lodash';
import React, { useEffect, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { LOADING_STATE } from '../../../enums/common.enum';
@ -113,8 +113,8 @@ const GlossaryVersion = ({ isGlossary = false }: GlossaryVersionProps) => {
isSummaryPanelOpen={false}
selectedData={selectedData as Glossary}
updateGlossary={() => Promise.resolve()}
onGlossaryDelete={noop}
onGlossaryTermDelete={noop}
onGlossaryDelete={() => Promise.resolve()}
onGlossaryTermDelete={() => Promise.resolve()}
onGlossaryTermUpdate={() => Promise.resolve()}
/>
)}

View File

@ -14,7 +14,7 @@
import { HTMLAttributes } from 'react';
export interface EntityDeleteModalProp extends HTMLAttributes<HTMLDivElement> {
onConfirm: () => void;
onConfirm: () => Promise<void>;
onCancel: () => void;
entityName: string;
entityType: string;

View File

@ -30,6 +30,7 @@ const EntityDeleteModal = ({
bodyText,
}: EntityDeleteModalProp) => {
const [name, setName] = useState('');
const [saving, setSaving] = useState(false);
const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
@ -42,6 +43,12 @@ const EntityDeleteModal = ({
[loadingState]
);
const handleSave = async () => {
setSaving(true);
await onConfirm();
setSaving(false);
};
// To remove the entered text in the modal input after modal closed
useEffect(() => {
setName('');
@ -67,9 +74,9 @@ const EntityDeleteModal = ({
<Button
data-testid={isLoadingWaiting ? 'loading-button' : 'confirm-button'}
disabled={!isNameMatching}
loading={isLoadingWaiting}
loading={saving}
type="primary"
onClick={onConfirm}>
onClick={handleSave}>
{t('label.confirm')}
</Button>
</div>

View File

@ -11,7 +11,6 @@
* limitations under the License.
*/
import { Form, FormProps, Input, Modal } from 'antd';
import { isUndefined, omit } from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
@ -23,9 +22,17 @@ import { StyleModalProps, StyleWithInput } from './StyleModal.interface';
const StyleModal = ({ open, onCancel, onSubmit, style }: StyleModalProps) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [saving, setSaving] = React.useState<boolean>(false);
const handleSubmit: FormProps<StyleWithInput>['onFinish'] = (value) => {
onSubmit(omit(value, 'colorInput'));
const handleSubmit: FormProps<StyleWithInput>['onFinish'] = async (value) => {
try {
setSaving(true);
await onSubmit(omit(value, 'colorInput'));
} catch (err) {
// Error is handled in parent component
} finally {
setSaving(false);
}
};
return (
@ -34,6 +41,7 @@ const StyleModal = ({ open, onCancel, onSubmit, style }: StyleModalProps) => {
okButtonProps={{
form: 'style-modal',
htmlType: 'submit',
loading: saving,
}}
okText={t('label.submit')}
open={open}

View File

@ -15,7 +15,7 @@ import { Style } from '../../../generated/type/schema';
export interface StyleModalProps {
open: boolean;
style?: Style;
onSubmit: (value: Style) => void;
onSubmit: (value: Style) => Promise<void>;
onCancel: () => void;
}

View File

@ -12,7 +12,6 @@
*/
import { Button, Popover, Space, Typography } from 'antd';
import { t } from 'i18next';
import { noop } from 'lodash';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg';
@ -46,7 +45,7 @@ export const PersonaListItemRenderer = (props: EntityReference) => {
export const PersonaSelectableList = ({
hasPermission,
selectedPersonas = [],
onUpdate = noop,
onUpdate,
children,
popoverProps,
multiSelect = false,
@ -96,11 +95,11 @@ export const PersonaSelectableList = ({
};
const handleUpdate = useCallback(
(users: EntityReference[]) => {
async (users: EntityReference[]) => {
if (multiSelect) {
(onUpdate as (users: EntityReference[]) => void)(users);
await (onUpdate as (users: EntityReference[]) => Promise<void>)(users);
} else {
(onUpdate as (users: EntityReference) => void)(users[0]);
await (onUpdate as (users: EntityReference) => Promise<void>)(users[0]);
}
setPopupVisible(false);

View File

@ -116,18 +116,18 @@ const Ingestion: React.FC<IngestionProps> = ({
});
};
const handleDelete = (id: string, displayName: string) => {
const handleDelete = async (id: string, displayName: string) => {
setDeleteSelection({ id, name: displayName, state: 'waiting' });
deleteIngestion(id, displayName)
.then(() => {
setTimeout(() => {
setDeleteSelection({ id, name: displayName, state: 'success' });
handleCancelConfirmationModal();
}, 500);
})
.catch(() => {
try {
await deleteIngestion(id, displayName);
setTimeout(() => {
setDeleteSelection({ id, name: displayName, state: 'success' });
handleCancelConfirmationModal();
});
}, 500);
} catch (error) {
handleCancelConfirmationModal();
}
};
const getSearchedIngestions = () => {

View File

@ -100,7 +100,7 @@ export interface TeamDetailsProp {
descriptionHandler: (value: boolean) => void;
onDescriptionUpdate: (value: string) => Promise<void>;
updateTeamHandler: (data: Team, fetchTeam?: boolean) => Promise<void>;
handleAddUser: (data: Array<EntityReference>) => void;
handleAddUser: (data: Array<EntityReference>) => Promise<void>;
afterDeleteAction: (isSoftDeleted?: boolean) => void;
removeUserFromTeam: (id: string) => Promise<void>;
handleJoinTeamClick: (id: string, data: Operation[]) => void;

View File

@ -17,6 +17,6 @@ import { EntityReference } from '../../../../../generated/type/entityReference';
export interface UserTabProps {
permission: OperationPermission;
currentTeam: Team;
onAddUser: (data: EntityReference[]) => void;
onAddUser: (data: EntityReference[]) => Promise<void>;
onRemoveUser: (id: string) => Promise<void>;
}

View File

@ -13,6 +13,7 @@
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { Button, Col, Form, Row, Space } from 'antd';
import { useForm } from 'antd/lib/form/Form';
import { DefaultOptionType } from 'antd/lib/select';
import React, { useState } from 'react';
import AsyncSelectList from '../../common/AsyncSelectList/AsyncSelectList';
import './tag-select-fom.style.less';
@ -30,15 +31,20 @@ const TagSelectForm = ({
const [form] = useForm();
const [isSubmitLoading, setIsSubmitLoading] = useState(false);
const handleSave = async (data: {
tags: DefaultOptionType | DefaultOptionType[];
}) => {
setIsSubmitLoading(true);
await onSubmit(data.tags);
setIsSubmitLoading(false);
};
return (
<Form
form={form}
initialValues={{ tags: defaultValue }}
name="tagsForm"
onFinish={(data) => {
setIsSubmitLoading(true);
onSubmit(data.tags);
}}>
onFinish={handleSave}>
<Row gutter={[0, 8]}>
<Col className="gutter-row d-flex justify-end" span={24}>
<Space align="center">

View File

@ -15,41 +15,26 @@ import Icon from '@ant-design/icons/lib/components/Icon';
import { Button, Space } from 'antd';
import Tooltip, { RenderFunction } from 'antd/lib/tooltip';
import classNames from 'classnames';
import {
isEmpty,
isString,
isUndefined,
lowerCase,
noop,
toLower,
} from 'lodash';
import { isEmpty, isString, isUndefined, lowerCase, toLower } from 'lodash';
import { ExtraInfo } from 'Models';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg';
import { ReactComponent as IconExternalLink } from '../../../assets/svg/external-links.svg';
import { ReactComponent as DomainIcon } from '../../../assets/svg/ic-domain.svg';
import { ReactComponent as IconInfoSecondary } from '../../../assets/svg/icon-info.svg';
import { ReactComponent as IconTeamsGrey } from '../../../assets/svg/teams-grey.svg';
import { DE_ACTIVE_COLOR } from '../../../constants/constants';
import { Tag } from '../../../generated/entity/classification/tag';
import { Dashboard } from '../../../generated/entity/data/dashboard';
import { Table } from '../../../generated/entity/data/table';
import { TagLabel } from '../../../generated/type/tagLabel';
import { getTeamsUser } from '../../../utils/CommonUtils';
import { useAuthContext } from '../../Auth/AuthProviders/AuthProvider';
import ProfilePicture from '../ProfilePicture/ProfilePicture';
import TierCard from '../TierCard/TierCard';
import { UserSelectableList } from '../UserSelectableList/UserSelectableList.component';
import { UserTeamSelectableList } from '../UserTeamSelectableList/UserTeamSelectableList.component';
import './entity-summary-details.style.less';
export interface GetInfoElementsProps {
data: ExtraInfo;
updateOwner?: (value: Table['owner']) => void;
tier?: TagLabel;
currentTier?: string;
updateTier?: (value?: Tag) => Promise<void>;
currentOwner?: Dashboard['owner'];
deleted?: boolean;
allowTeamOwner?: boolean;
@ -69,35 +54,12 @@ const InfoIcon = ({
</Tooltip>
);
const EntitySummaryDetails = ({
data,
tier,
updateOwner,
updateTier,
currentOwner,
deleted = false,
allowTeamOwner = true,
}: GetInfoElementsProps) => {
const EntitySummaryDetails = ({ data }: GetInfoElementsProps) => {
let retVal = <></>;
const { t } = useTranslation();
const { currentUser } = useAuthContext();
const displayVal = data.placeholderText || data.value;
const ownerDropdown = allowTeamOwner ? (
<UserTeamSelectableList
hasPermission={Boolean(updateOwner)}
owner={currentOwner}
onUpdate={updateOwner ?? noop}
/>
) : (
<UserSelectableList
hasPermission={Boolean(updateOwner)}
multiSelect={false}
selectedUsers={currentOwner ? [currentOwner] : []}
onUpdate={updateOwner ?? noop}
/>
);
const { isEntityDetails, userDetails, isTier, isOwner, isTeamOwner } =
useMemo(() => {
const userDetails = currentUser ? getTeamsUser(data, currentUser) : {};
@ -158,7 +120,6 @@ const EntitySummaryDetails = ({
className="d-flex gap-1 items-center"
data-testid="owner-link">
{t('label.no-entity', { entity: t('label.owner') })}
{updateOwner && !deleted ? ownerDropdown : null}
</span>
);
}
@ -169,20 +130,7 @@ const EntitySummaryDetails = ({
{
retVal =
!displayVal || displayVal === '--' ? (
<>
{t('label.no-entity', { entity: t('label.tier') })}
{updateTier && !deleted ? (
<TierCard currentTier={tier?.tagFQN} updateTier={updateTier}>
<span data-testid="edit-tier">
<EditIcon
className="cursor-pointer"
color={DE_ACTIVE_COLOR}
width={14}
/>
</span>
</TierCard>
) : null}
</>
<>{t('label.no-entity', { entity: t('label.tier') })}</>
) : (
<></>
);
@ -289,8 +237,6 @@ const EntitySummaryDetails = ({
}
/>
) : null}
{/* Edit icon with dropdown */}
{(isOwner || isTier) && (updateOwner ? ownerDropdown : null)}
</>
) : isOwner ? (
<>
@ -307,8 +253,6 @@ const EntitySummaryDetails = ({
{displayVal}
</Button>
</span>
{/* Edit icon with dropdown */}
{updateOwner ? ownerDropdown : null}
</>
) : isTier ? (
<Space
@ -322,18 +266,6 @@ const EntitySummaryDetails = ({
direction="horizontal"
title={displayVal as string}>
<span data-testid="Tier">{displayVal}</span>
{updateTier && !deleted ? (
<TierCard currentTier={tier?.tagFQN} updateTier={updateTier}>
<span data-testid="edit-tier">
<EditIcon
className="cursor-pointer"
color={DE_ACTIVE_COLOR}
width={14}
/>
</span>
</TierCard>
) : null}
</Space>
) : (
<span>{displayVal}</span>

View File

@ -260,7 +260,10 @@ export const SelectableList = ({
/>
}
itemLayout="vertical"
loading={{ spinning: fetching || updating, indicator: <Loader /> }}
loading={{
spinning: fetching || updating,
indicator: <Loader size="small" />,
}}
locale={{
emptyText: emptyPlaceholderText ?? t('message.no-data-available'),
}}

View File

@ -22,7 +22,7 @@ export interface SelectableListProps {
multiSelect?: boolean;
selectedItems: EntityReference[];
onCancel: () => void;
onUpdate: (updatedItems: EntityReference[]) => void | Promise<void>;
onUpdate: (updatedItems: EntityReference[]) => Promise<void>;
searchPlaceholder?: string;
customTagRenderer?: (props: EntityReference) => ReactNode;
searchBarDataTestId?: string;

View File

@ -11,7 +11,6 @@
* limitations under the License.
*/
import { Button, Popover, Tooltip } from 'antd';
import { noop } from 'lodash';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg';
@ -35,7 +34,7 @@ import { UserSelectableListProps } from './UserSelectableList.interface';
export const UserSelectableList = ({
hasPermission,
selectedUsers = [],
onUpdate = noop,
onUpdate,
children,
popoverProps,
multiSelect = true,
@ -100,11 +99,11 @@ export const UserSelectableList = ({
};
const handleUpdate = useCallback(
(users: EntityReference[]) => {
async (users: EntityReference[]) => {
if (multiSelect) {
(onUpdate as (users: EntityReference[]) => void)(users);
await (onUpdate as (users: EntityReference[]) => Promise<void>)(users);
} else {
(onUpdate as (users: EntityReference) => void)(users[0]);
await (onUpdate as (users: EntityReference) => Promise<void>)(users[0]);
}
setPopupVisible(false);
},

View File

@ -24,10 +24,10 @@ export type UserSelectableListProps =
} & (
| {
multiSelect?: true;
onUpdate: (updatedUsers: EntityReference[]) => void;
onUpdate: (updatedUsers: EntityReference[]) => Promise<void>;
}
| {
multiSelect: false;
onUpdate: (updatedUsers: EntityReference) => void;
onUpdate: (updatedUsers: EntityReference) => Promise<void>;
}
);

View File

@ -293,35 +293,36 @@ const GlossaryPage = () => {
[selectedData]
);
const handleGlossaryTermDelete = (id: string) => {
setDeleteStatus(LOADING_STATE.WAITING);
deleteGlossaryTerm(id)
.then(() => {
setDeleteStatus(LOADING_STATE.SUCCESS);
showSuccessToast(
t('server.entity-deleted-successfully', {
entity: t('label.glossary-term'),
})
);
let fqn;
if (glossaryFqn) {
const fqnArr = Fqn.split(glossaryFqn);
fqnArr.pop();
fqn = fqnArr.join(FQN_SEPARATOR_CHAR);
}
setIsLoading(true);
history.push(getGlossaryPath(fqn));
fetchGlossaryList();
})
.catch((err: AxiosError) => {
showErrorToast(
err,
t('server.delete-entity-error', {
entity: t('label.glossary-term'),
})
);
})
.finally(() => setDeleteStatus(LOADING_STATE.INITIAL));
const handleGlossaryTermDelete = async (id: string) => {
try {
setDeleteStatus(LOADING_STATE.WAITING);
await deleteGlossaryTerm(id);
setDeleteStatus(LOADING_STATE.SUCCESS);
showSuccessToast(
t('server.entity-deleted-successfully', {
entity: t('label.glossary-term'),
})
);
let fqn;
if (glossaryFqn) {
const fqnArr = Fqn.split(glossaryFqn);
fqnArr.pop();
fqn = fqnArr.join(FQN_SEPARATOR_CHAR);
}
setIsLoading(true);
history.push(getGlossaryPath(fqn));
fetchGlossaryList();
} catch (err) {
showErrorToast(
err,
t('server.delete-entity-error', {
entity: t('label.glossary-term'),
})
);
} finally {
setDeleteStatus(LOADING_STATE.INITIAL);
}
};
const handleAssetClick = useCallback(

View File

@ -201,7 +201,7 @@ const IncidentManagerDetailPage = () => {
const jsonPatch = compare(data, updatedTestCase);
if (jsonPatch.length && data.id) {
updateTestCase(data.id, jsonPatch);
await updateTestCase(data.id, jsonPatch);
}
}
};

View File

@ -13,6 +13,7 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { EntityType } from '../../enums/entity.enum';
import {
Database,
@ -32,7 +33,7 @@ const mockParams = {
};
jest.mock('react-router-dom', () => ({
Link: jest.fn().mockImplementation(({ children }) => <div>{children}</div>),
...jest.requireActual('react-router-dom'),
useParams: jest.fn().mockImplementation(() => mockParams),
}));
@ -147,7 +148,9 @@ const props: ServiceVersionMainTabContentProps = {
describe('ServiceVersionMainTabContent tests', () => {
it('Component should render properly provided proper data', () => {
render(<ServiceVersionMainTabContent {...props} />);
render(<ServiceVersionMainTabContent {...props} />, {
wrapper: MemoryRouter,
});
const entityTable = screen.getByTestId('service-children-table');
const entityName = screen.getByText('ecommerce_db');
@ -168,7 +171,9 @@ describe('ServiceVersionMainTabContent tests', () => {
});
it('Loader should be displayed if isServiceLoading is true', async () => {
render(<ServiceVersionMainTabContent {...props} isServiceLoading />);
render(<ServiceVersionMainTabContent {...props} isServiceLoading />, {
wrapper: MemoryRouter,
});
const loader = await screen.findByTestId('skeleton-table');

View File

@ -221,7 +221,7 @@ describe('TestDetailsPageV1 component', () => {
});
expect(getTableDetailsByFQN).toHaveBeenCalledWith('fqn', {
fields: `${COMMON_API_FIELDS}`,
fields: `${COMMON_API_FIELDS},usageSummary`,
});
});

View File

@ -159,7 +159,7 @@ const TableDetailsPageV1 = () => {
} finally {
setLoading(false);
}
}, [tableFqn]);
}, [tableFqn, viewUsagePermission]);
const fetchQueryCount = async () => {
if (!tableDetails?.id) {

View File

@ -12,7 +12,7 @@
*/
import { Form, Modal, Typography } from 'antd';
import React, { useEffect, useMemo } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { VALIDATION_MESSAGES } from '../../constants/constants';
import {
@ -22,7 +22,7 @@ import {
import { DEFAULT_FORM_VALUE } from '../../constants/Tags.constant';
import { FieldProp, FieldTypes } from '../../interface/FormUtils.interface';
import { generateFormFields } from '../../utils/formUtils';
import { RenameFormProps } from './TagsPage.interface';
import { RenameFormProps, SubmitProps } from './TagsPage.interface';
const TagsForm = ({
visible,
@ -40,6 +40,7 @@ const TagsForm = ({
}: RenameFormProps) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [saving, setSaving] = useState(false);
useEffect(() => {
form.setFieldsValue({
@ -202,6 +203,18 @@ const TagsForm = ({
: []),
];
const handleSave = async (data: SubmitProps) => {
try {
setSaving(true);
await onSubmit(data);
form.setFieldsValue(DEFAULT_FORM_VALUE);
} catch {
// Parent will handle the error
} finally {
setSaving(false);
}
};
return (
<Modal
centered
@ -212,7 +225,7 @@ const TagsForm = ({
form: 'tags',
type: 'primary',
htmlType: 'submit',
loading: isLoading,
loading: isLoading || saving,
}}
okText={t('label.save')}
open={visible}
@ -232,10 +245,7 @@ const TagsForm = ({
layout="vertical"
name="tags"
validateMessages={VALIDATION_MESSAGES}
onFinish={(data) => {
onSubmit(data);
form.setFieldsValue(DEFAULT_FORM_VALUE);
}}>
onFinish={handleSave}>
{generateFormFields(formFields)}
</Form>
</Modal>

View File

@ -44,7 +44,7 @@ export interface RenameFormProps {
onCancel: () => void;
header: string;
initialValues?: Tag;
onSubmit: (value: SubmitProps) => void;
onSubmit: (value: SubmitProps) => Promise<void>;
showMutuallyExclusive?: boolean;
isClassification?: boolean;
data?: Classification[];

View File

@ -267,56 +267,55 @@ const TagsPage = () => {
* @param categoryName - tag category name
* @param tagId - tag id
*/
const handleDeleteTag = (tagId: string) => {
deleteTag(tagId)
.then((res) => {
if (res) {
if (currentClassification) {
setDeleteStatus(LOADING_STATE.SUCCESS);
setClassifications((prev) =>
prev.map((item) => {
if (
item.fullyQualifiedName ===
currentClassification.fullyQualifiedName
) {
return {
...item,
termCount: (item.termCount ?? 0) - 1,
};
}
const handleDeleteTag = async (tagId: string) => {
try {
const res = await deleteTag(tagId);
return item;
})
);
}
classificationDetailsRef.current?.refreshClassificationTags();
} else {
showErrorToast(
t('server.delete-entity-error', {
entity: t('label.tag-lowercase'),
if (res) {
if (currentClassification) {
setDeleteStatus(LOADING_STATE.SUCCESS);
setClassifications((prev) =>
prev.map((item) => {
if (
item.fullyQualifiedName ===
currentClassification.fullyQualifiedName
) {
return {
...item,
termCount: (item.termCount ?? 0) - 1,
};
}
return item;
})
);
}
})
.catch((err: AxiosError) => {
classificationDetailsRef.current?.refreshClassificationTags();
} else {
showErrorToast(
err,
t('server.delete-entity-error', { entity: t('label.tag-lowercase') })
t('server.delete-entity-error', {
entity: t('label.tag-lowercase'),
})
);
})
.finally(() => {
setDeleteTags({ data: undefined, state: false });
setDeleteStatus(LOADING_STATE.INITIAL);
});
}
} catch (err) {
showErrorToast(
err,
t('server.delete-entity-error', { entity: t('label.tag-lowercase') })
);
} finally {
setDeleteTags({ data: undefined, state: false });
setDeleteStatus(LOADING_STATE.INITIAL);
}
};
/**
* It redirects to respective function call based on tag/Classification
*/
const handleConfirmClick = () => {
const handleConfirmClick = async () => {
if (deleteTags.data?.id) {
setDeleteStatus(LOADING_STATE.WAITING);
handleDeleteTag(deleteTags.data.id);
await handleDeleteTag(deleteTags.data.id);
}
};
@ -523,16 +522,16 @@ const TagsPage = () => {
history.push(getTagPath(category.fullyQualifiedName));
};
const handleAddTagSubmit = (data: SubmitProps) => {
const handleAddTagSubmit = async (data: SubmitProps) => {
const updatedData = omit(data, 'color', 'iconURL');
const style = {
color: data.color,
iconURL: data.iconURL,
};
if (editTag) {
handleUpdatePrimaryTag({ ...editTag, ...updatedData, style });
await handleUpdatePrimaryTag({ ...editTag, ...updatedData, style });
} else {
handleCreatePrimaryTag({ ...updatedData, style });
await handleCreatePrimaryTag({ ...updatedData, style });
}
};

View File

@ -25,7 +25,6 @@ import { Bucket, EntityDetailUnion } from 'Models';
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import { OwnerLabel } from '../components/common/OwnerLabel/OwnerLabel.component';
import ProfilePicture from '../components/common/ProfilePicture/ProfilePicture';
import QueryCount from '../components/common/QueryCount/QueryCount.component';
import { DataAssetsWithoutServiceField } from '../components/DataAssets/DataAssetsHeader/DataAssetsHeader.interface';
import { QueryVoteType } from '../components/Database/TableQueries/TableQueries.interface';
@ -174,21 +173,6 @@ export const getEntityTags = (
}
};
export const getOwnerNameWithProfilePic = (
owner: EntityReference | undefined
) =>
owner ? (
<div className="flex items-center gap-2">
{' '}
<ProfilePicture
displayName={owner.displayName}
name={owner.name ?? ''}
width="20"
/>
<span>{getEntityName(owner)}</span>
</div>
) : null;
const getUsageData = (usageSummary: UsageDetails | undefined) =>
!isNil(usageSummary?.weeklyStats?.percentileRank)
? getUsagePercentile(usageSummary?.weeklyStats?.percentileRank ?? 0)
@ -347,11 +331,7 @@ const getPipelineOverview = (pipelineDetails: Pipeline) => {
const overview = [
{
name: i18next.t('label.owner'),
value:
getOwnerNameWithProfilePic(owner) ??
i18next.t('label.no-entity', {
entity: i18next.t('label.owner'),
}),
value: <OwnerLabel hasPermission={false} owner={owner} />,
url: getOwnerValue(owner as EntityReference),
isLink: !isEmpty(owner?.name),
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
@ -400,11 +380,7 @@ const getDashboardOverview = (dashboardDetails: Dashboard) => {
const overview = [
{
name: i18next.t('label.owner'),
value:
getOwnerNameWithProfilePic(owner) ??
i18next.t('label.no-entity', {
entity: i18next.t('label.owner'),
}),
value: <OwnerLabel hasPermission={false} owner={owner} />,
url: getOwnerValue(owner as EntityReference),
isLink: !isEmpty(owner?.name),
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
@ -455,11 +431,7 @@ export const getSearchIndexOverview = (
const overview = [
{
name: i18next.t('label.owner'),
value:
getOwnerNameWithProfilePic(owner) ??
i18next.t('label.no-entity', {
entity: i18next.t('label.owner'),
}),
value: <OwnerLabel hasPermission={false} owner={owner} />,
url: getOwnerValue(owner as EntityReference),
isLink: !isEmpty(owner?.name),
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
@ -493,11 +465,7 @@ const getMlModelOverview = (mlModelDetails: Mlmodel) => {
const overview = [
{
name: i18next.t('label.owner'),
value:
getOwnerNameWithProfilePic(owner) ??
i18next.t('label.no-entity', {
entity: i18next.t('label.owner'),
}),
value: <OwnerLabel hasPermission={false} owner={owner} />,
url: getOwnerValue(owner as EntityReference),
isLink: !isEmpty(owner?.name),
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
@ -598,11 +566,7 @@ const getDataModelOverview = (dataModelDetails: DashboardDataModel) => {
const overview = [
{
name: i18next.t('label.owner'),
value:
getOwnerNameWithProfilePic(owner) ??
i18next.t('label.no-entity', {
entity: i18next.t('label.owner'),
}),
value: <OwnerLabel hasPermission={false} owner={owner} />,
url: getOwnerValue(owner as EntityReference),
isLink: !isEmpty(owner?.name),
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
@ -675,11 +639,7 @@ const getStoredProcedureOverview = (
const overview = [
{
name: i18next.t('label.owner'),
value:
getOwnerNameWithProfilePic(owner) ??
i18next.t('label.no-entity', {
entity: i18next.t('label.owner'),
}),
value: <OwnerLabel hasPermission={false} owner={owner} />,
url: getOwnerValue(owner as EntityReference),
isLink: !isEmpty(owner?.name),
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
@ -757,11 +717,7 @@ const getDatabaseOverview = (databaseDetails: Database) => {
const overview = [
{
name: i18next.t('label.owner'),
value:
getOwnerNameWithProfilePic(owner) ??
i18next.t('label.no-entity', {
entity: i18next.t('label.owner'),
}),
value: <OwnerLabel hasPermission={false} owner={owner} />,
url: getOwnerValue(owner as EntityReference),
isLink: !isEmpty(owner?.name),
visible: [DRAWER_NAVIGATION_OPTIONS.explore],
@ -804,11 +760,7 @@ const getDatabaseSchemaOverview = (databaseSchemaDetails: DatabaseSchema) => {
const overview = [
{
name: i18next.t('label.owner'),
value:
getOwnerNameWithProfilePic(owner) ??
i18next.t('label.no-entity', {
entity: i18next.t('label.owner'),
}),
value: <OwnerLabel hasPermission={false} owner={owner} />,
url: getOwnerValue(owner as EntityReference),
isLink: !isEmpty(owner?.name),
visible: [DRAWER_NAVIGATION_OPTIONS.explore],
@ -856,11 +808,7 @@ const getEntityServiceOverview = (serviceDetails: EntityServiceUnion) => {
const overview = [
{
name: i18next.t('label.owner'),
value:
getOwnerNameWithProfilePic(owner) ??
i18next.t('label.no-entity', {
entity: i18next.t('label.owner'),
}),
value: <OwnerLabel hasPermission={false} owner={owner} />,
url: getOwnerValue(owner as EntityReference),
isLink: !isEmpty(owner?.name),
visible: [DRAWER_NAVIGATION_OPTIONS.explore],

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { Space, Typography } from 'antd';
import { Typography } from 'antd';
import {
ArrayChange,
Change,
@ -31,15 +31,11 @@ import {
} from 'lodash';
import React, { Fragment, ReactNode } from 'react';
import ReactDOMServer from 'react-dom/server';
import { Link } from 'react-router-dom';
import { ReactComponent as IconTeamsGrey } from '../assets/svg/teams-grey.svg';
import {
ExtentionEntities,
ExtentionEntitiesKeys,
} from '../components/common/CustomPropertyTable/CustomPropertyTable.interface';
import ProfilePicture from '../components/common/ProfilePicture/ProfilePicture';
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
import { getTeamAndUserDetailsPath, getUserPath } from '../constants/constants';
import { EntityField } from '../constants/Feeds.constants';
import { EntityType } from '../enums/entity.enum';
import { Column as ContainerColumn } from '../generated/entity/data/container';
@ -522,33 +518,6 @@ export function getEntityTagDiff<
return entityList ?? [];
}
export const getOwnerInfo = (owner: EntityReference, ownerLabel: ReactNode) => {
const isTeamType = owner.type === 'team';
return (
<Space className="m-r-xss" size={4}>
{isTeamType ? (
<IconTeamsGrey height={18} width={18} />
) : (
<ProfilePicture
displayName={getEntityName(owner)}
name={owner.name ?? ''}
textClass="text-xs"
width="20"
/>
)}
<Link
to={
isTeamType
? getTeamAndUserDetailsPath(owner.name ?? '')
: getUserPath(owner.name ?? '')
}>
{ownerLabel}
</Link>
</Space>
);
};
export const getEntityReferenceDiffFromFieldName = (
fieldName: string,
changeDescription: ChangeDescription,

View File

@ -11,14 +11,15 @@
* limitations under the License.
*/
import { Space, Typography } from 'antd';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { t } from 'i18next';
import { isUndefined } from 'lodash';
import { ServiceTypes } from 'Models';
import React from 'react';
import { Link } from 'react-router-dom';
import ProfilePicture from '../components/common/ProfilePicture/ProfilePicture';
import { UserTeam } from '../components/common/AssigneeList/AssigneeList.interface';
import UserPopOverCard from '../components/common/PopOverCard/UserPopOverCard';
import RichTextEditorPreviewer from '../components/common/RichTextEditor/RichTextEditorPreviewer';
import TagsViewer from '../components/Tag/TagsViewer/TagsViewer';
import { NO_DATA_PLACEHOLDER } from '../constants/constants';
@ -92,12 +93,14 @@ export const getServiceMainTabColumns = (
key: 'owner',
render: (owner: ServicePageData['owner']) =>
!isUndefined(owner) ? (
<Space data-testid="owner-data">
<ProfilePicture name={owner.name ?? ''} width="24" />
<Typography.Text data-testid={`${owner.name}-owner-name`}>
{getEntityName(owner)}
</Typography.Text>
</Space>
<UserPopOverCard
showUserName
data-testid="owner-data"
displayName={owner.displayName}
profileWidth={20}
type={owner.type as UserTeam}
userName={owner.name ?? ''}
/>
) : (
<Typography.Text data-testid="no-owner-text">--</Typography.Text>
),