chore(ui): update add button styling for expandable cards (#21197)

* chore(ui): update add button styling for expandable cards

* fix tests

* fix test for expandable card

* fix data test id

* fix playwright tests

* fix tests

* fix metric playwright

* fix test
This commit is contained in:
Chirag Madlani 2025-05-16 20:37:22 +05:30 committed by GitHub
parent 76834ce90a
commit b74816ceed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 769 additions and 942 deletions

View File

@ -211,10 +211,14 @@ entities.forEach((EntityClass) => {
});
test('Tag Add, Update and Remove', async ({ page }) => {
test.slow(true);
await entity.tag(page, 'PersonalData.Personal', 'PII.None');
});
test('Glossary Term Add, Update and Remove', async ({ page }) => {
test.slow(true);
await entity.glossaryTerm(
page,
EntityDataClass.glossaryTerm1.responseData,

View File

@ -224,7 +224,7 @@ export class EntityClass {
await page
.getByTestId('KnowledgePanel.Tags')
.getByTestId('tags-container')
.getByTestId('Add')
.getByTestId('add-tag')
.isVisible();
}
@ -265,7 +265,7 @@ export class EntityClass {
await page
.locator(`[${rowSelector}="${rowId}"]`)
.getByTestId('tags-container')
.getByTestId('Add')
.getByTestId('add-tag')
.isVisible();
}
@ -281,7 +281,7 @@ export class EntityClass {
await page
.getByTestId('KnowledgePanel.GlossaryTerms')
.getByTestId('glossary-container')
.getByTestId('Add')
.getByTestId('add-tag')
.isVisible();
}
@ -321,7 +321,7 @@ export class EntityClass {
await page
.locator(`[${rowSelector}="${rowId}"]`)
.getByTestId('glossary-container')
.getByTestId('Add')
.getByTestId('add-tag')
.isVisible();
}

View File

@ -532,14 +532,14 @@ export const checkDataConsumerPermissions = async (page: Page) => {
// Check right panel add tags button
await expect(
page.locator(
'[data-testid="KnowledgePanel.Tags"] [data-testid="tags-container"] [data-testid="entity-tags"] .tag-chip-add-button'
'[data-testid="KnowledgePanel.Tags"] [data-testid="tags-container"] [data-testid="add-tag"]'
)
).toBeVisible();
// Check right panel add glossary term button
await expect(
page.locator(
'[data-testid="KnowledgePanel.GlossaryTerms"] [data-testid="glossary-container"] [data-testid="entity-tags"] .tag-chip-add-button'
'[data-testid="KnowledgePanel.GlossaryTerms"] [data-testid="glossary-container"] [data-testid="add-tag"]'
)
).toBeVisible();
@ -617,14 +617,14 @@ export const checkStewardPermissions = async (page: Page) => {
// Check right panel add tags button
await expect(
page.locator(
'[data-testid="KnowledgePanel.Tags"] [data-testid="tags-container"] [data-testid="entity-tags"] .tag-chip-add-button'
'[data-testid="KnowledgePanel.Tags"] [data-testid="tags-container"] [data-testid="add-tag"]'
)
).toBeVisible();
// Check right panel add glossary term button
await expect(
page.locator(
'[data-testid="KnowledgePanel.GlossaryTerms"] [data-testid="glossary-container"] [data-testid="entity-tags"] .tag-chip-add-button'
'[data-testid="KnowledgePanel.GlossaryTerms"] [data-testid="glossary-container"] [data-testid="add-tag"]'
)
).toBeVisible();

View File

@ -14,13 +14,14 @@ import { Typography } from 'antd';
import { t } from 'i18next';
import { isEmpty } from 'lodash';
import React, { useMemo } from 'react';
import { ReactComponent as PlusIcon } from '../../../assets/svg/plus-primary.svg';
import { TabSpecificField } from '../../../enums/entity.enum';
import { EntityReference } from '../../../generated/entity/type';
import { getOwnerVersionLabel } from '../../../utils/EntityVersionUtils';
import ExpandableCard from '../../common/ExpandableCard/ExpandableCard';
import { EditIconButton } from '../../common/IconButtons/EditIconButton';
import TagButton from '../../common/TagButton/TagButton.component';
import {
EditIconButton,
PlusIconButton,
} from '../../common/IconButtons/EditIconButton';
import { UserTeamSelectableList } from '../../common/UserTeamSelectableList/UserTeamSelectableList.component';
import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider';
@ -48,15 +49,22 @@ export const OwnerLabelV2 = <
<Typography.Text className="text-sm font-medium">
{t('label.owner-plural')}
</Typography.Text>
{(permissions.EditOwners || permissions.EditAll) &&
data.owners &&
data.owners.length > 0 && (
<UserTeamSelectableList
hasPermission={permissions.EditOwners || permissions.EditAll}
listHeight={200}
multiple={{ user: true, team: false }}
owner={data.owners}
onUpdate={handleUpdatedOwner}>
{(permissions.EditOwners || permissions.EditAll) && (
<UserTeamSelectableList
hasPermission={permissions.EditOwners || permissions.EditAll}
listHeight={200}
multiple={{ user: true, team: false }}
owner={data.owners}
onUpdate={handleUpdatedOwner}>
{isEmpty(data.owners) ? (
<PlusIconButton
data-testid="add-owner"
size="small"
title={t('label.add-entity', {
entity: t('label.owner-plural'),
})}
/>
) : (
<EditIconButton
newLook
data-testid="edit-owner"
@ -65,8 +73,9 @@ export const OwnerLabelV2 = <
entity: t('label.owner-plural'),
})}
/>
</UserTeamSelectableList>
)}
)}
</UserTeamSelectableList>
)}
</div>
),
[data, permissions, handleUpdatedOwner]
@ -85,24 +94,6 @@ export const OwnerLabelV2 = <
TabSpecificField.OWNERS,
permissions.EditOwners || permissions.EditAll
)}
{data.owners?.length === 0 &&
(permissions.EditOwners || permissions.EditAll) && (
<UserTeamSelectableList
hasPermission={permissions.EditOwners || permissions.EditAll}
listHeight={200}
multiple={{ user: true, team: false }}
owner={data.owners}
onUpdate={(updatedUser) => handleUpdatedOwner(updatedUser)}>
<TagButton
className="text-primary cursor-pointer"
dataTestId="add-owner"
icon={<PlusIcon height={16} name="plus" width={16} />}
label={t('label.add')}
tooltip=""
/>
</UserTeamSelectableList>
)}
</ExpandableCard>
);
};

View File

@ -13,14 +13,15 @@
import { Typography } from 'antd';
import { t } from 'i18next';
import React, { useMemo } from 'react';
import { ReactComponent as PlusIcon } from '../../../assets/svg/plus-primary.svg';
import { TabSpecificField } from '../../../enums/entity.enum';
import { EntityReference } from '../../../generated/entity/type';
import { ChangeDescription } from '../../../generated/type/changeEvent';
import { getOwnerVersionLabel } from '../../../utils/EntityVersionUtils';
import ExpandableCard from '../../common/ExpandableCard/ExpandableCard';
import { EditIconButton } from '../../common/IconButtons/EditIconButton';
import TagButton from '../../common/TagButton/TagButton.component';
import {
EditIconButton,
PlusIconButton,
} from '../../common/IconButtons/EditIconButton';
import { UserTeamSelectableList } from '../../common/UserTeamSelectableList/UserTeamSelectableList.component';
import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider';
@ -70,7 +71,7 @@ export const ReviewerLabelV2 = <
data-testid="heading-name">
{t('label.reviewer-plural')}
</Typography.Text>
{hasEditReviewerAccess && hasReviewers && (
{hasEditReviewerAccess && (
<UserTeamSelectableList
previewSelected
hasPermission={hasEditReviewerAccess}
@ -80,14 +81,24 @@ export const ReviewerLabelV2 = <
owner={assignedReviewers ?? []}
popoverProps={{ placement: 'topLeft' }}
onUpdate={handleReviewerSave}>
<EditIconButton
newLook
data-testid="edit-reviewer-button"
size="small"
title={t('label.edit-entity', {
entity: t('label.reviewer-plural'),
})}
/>
{hasReviewers ? (
<EditIconButton
newLook
data-testid="edit-reviewer-button"
size="small"
title={t('label.edit-entity', {
entity: t('label.reviewer-plural'),
})}
/>
) : (
<PlusIconButton
data-testid="Add"
size="small"
title={t('label.add-entity', {
entity: t('label.reviewer-plural'),
})}
/>
)}
</UserTeamSelectableList>
)}
</div>
@ -102,33 +113,16 @@ export const ReviewerLabelV2 = <
}}
dataTestId="glossary-reviewer"
isExpandDisabled={!hasReviewers}>
<div data-testid="glossary-reviewer-name">
{getOwnerVersionLabel(
data,
isVersionView ?? false,
TabSpecificField.REVIEWERS,
hasEditReviewerAccess
)}
</div>
{hasEditReviewerAccess && !hasReviewers && (
<UserTeamSelectableList
previewSelected
hasPermission={hasEditReviewerAccess}
label={t('label.reviewer-plural')}
listHeight={200}
multiple={{ user: true, team: false }}
owner={assignedReviewers ?? []}
popoverProps={{ placement: 'topLeft' }}
onUpdate={handleReviewerSave}>
<TagButton
className="text-primary cursor-pointer"
icon={<PlusIcon height={16} name="plus" width={16} />}
label={t('label.add')}
tooltip=""
/>
</UserTeamSelectableList>
)}
{hasReviewers ? (
<div data-testid="glossary-reviewer-name">
{getOwnerVersionLabel(
data,
isVersionView ?? false,
TabSpecificField.REVIEWERS,
hasEditReviewerAccess
)}
</div>
) : null}
</ExpandableCard>
);
};

View File

@ -1,30 +0,0 @@
/*
* 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 { DataProduct } from '../../../generated/entity/domains/dataProduct';
import { Paging } from '../../../generated/type/paging';
import { DataProductSelectOption } from '../DataProductsSelectList/DataProductSelectList.interface';
export type DataProductsSelectFormProps = {
placeholder: string;
defaultValue: string[];
onChange?: (value: string[]) => void;
onSubmit: (values: DataProduct[]) => Promise<void>;
onCancel: () => void;
fetchApi: (
search: string,
page: number
) => Promise<{
data: DataProductSelectOption[];
paging: Paging;
}>;
};

View File

@ -1,79 +0,0 @@
/*
* 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 { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import DataProductsSelectForm from './DataProductsSelectForm';
const mockOnSubmit = jest.fn();
const mockOnCancel = jest.fn();
describe('Data Products Form Page', () => {
it('renders without errors', () => {
const { getByTestId } = render(
<DataProductsSelectForm
defaultValue={[]}
fetchApi={() => Promise.resolve({ data: [], paging: { total: 0 } })}
placeholder="Select products"
onCancel={mockOnCancel}
onSubmit={mockOnSubmit}
/>
);
// Ensure that the component renders without errors
expect(getByTestId('data-product-selector')).toBeInTheDocument();
});
it('calls onCancel function when Cancel button is clicked', () => {
const { getByTestId } = render(
<DataProductsSelectForm
defaultValue={[]}
fetchApi={() => Promise.resolve({ data: [], paging: { total: 0 } })}
placeholder="Select products"
onCancel={mockOnCancel}
onSubmit={mockOnSubmit}
/>
);
const cancelButton = getByTestId('cancelAssociatedTag');
userEvent.click(cancelButton);
// Ensure that the onCancel function is called when the Cancel button is clicked
expect(mockOnCancel).toHaveBeenCalledTimes(1);
});
it('calls onSubmit when the Save button is clicked', () => {
const { getByTestId } = render(
<DataProductsSelectForm
defaultValue={[]}
fetchApi={() => Promise.resolve({ data: [], paging: { total: 0 } })}
placeholder="Select products"
onCancel={mockOnCancel}
onSubmit={mockOnSubmit}
/>
);
const selectRef = getByTestId('data-product-selector').querySelector(
'.ant-select-selector'
);
if (selectRef) {
userEvent.click(selectRef);
}
// Simulate the Save button click
userEvent.click(getByTestId('saveAssociatedTag'));
// Check if onSubmit was called with the selected value
expect(mockOnSubmit).toHaveBeenCalledTimes(1);
expect(mockOnSubmit).toHaveBeenCalledWith(expect.any(Array));
});
});

View File

@ -1,74 +0,0 @@
/*
* 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 { CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { Button, Col, Row, Space } from 'antd';
import React, { useRef, useState } from 'react';
import { DataProductsSelectRef } from '../DataProductsSelectList/DataProductSelectList.interface';
import DataProductsSelectList from '../DataProductsSelectList/DataProductsSelectList';
import { DataProductsSelectFormProps } from './DataProductsSelectForm.interface';
const DataProductsSelectForm = ({
fetchApi,
defaultValue,
placeholder,
onSubmit,
onCancel,
}: DataProductsSelectFormProps) => {
const [isSubmitLoading, setIsSubmitLoading] = useState(false);
const selectRef = useRef<DataProductsSelectRef>(null);
const onSave = () => {
setIsSubmitLoading(true);
const value = selectRef.current?.getSelectValue() ?? [];
onSubmit(value);
};
return (
<Row gutter={[0, 8]}>
<Col className="gutter-row d-flex justify-end" span={24}>
<Space align="center">
<Button
className="p-x-05"
data-testid="cancelAssociatedTag"
disabled={isSubmitLoading}
icon={<CloseOutlined size={12} />}
size="small"
onClick={onCancel}
/>
<Button
className="p-x-05"
data-testid="saveAssociatedTag"
icon={<CheckOutlined size={12} />}
loading={isSubmitLoading}
size="small"
type="primary"
onClick={onSave}
/>
</Space>
</Col>
<Col className="gutter-row" span={24}>
<DataProductsSelectList
defaultValue={defaultValue}
fetchOptions={fetchApi}
mode="multiple"
placeholder={placeholder}
ref={selectRef}
/>
</Col>
</Row>
);
};
export default DataProductsSelectForm;

View File

@ -18,7 +18,6 @@ import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { ReactComponent as DataProductIcon } from '../../../assets/svg/ic-data-product.svg';
import { NO_DATA_PLACEHOLDER } from '../../../constants/constants';
import { TAG_CONSTANT, TAG_START_WITH } from '../../../constants/Tag.constants';
import { EntityType } from '../../../enums/entity.enum';
import { DataProduct } from '../../../generated/entity/domains/dataProduct';
import { EntityReference } from '../../../generated/entity/type';
@ -26,9 +25,11 @@ import { fetchDataProductsElasticSearch } from '../../../rest/dataProductAPI';
import { getEntityName } from '../../../utils/EntityUtils';
import { getEntityDetailsPath } from '../../../utils/RouterUtils';
import ExpandableCard from '../../common/ExpandableCard/ExpandableCard';
import { EditIconButton } from '../../common/IconButtons/EditIconButton';
import TagsV1 from '../../Tag/TagsV1/TagsV1.component';
import DataProductsSelectForm from '../DataProductSelectForm/DataProductsSelectForm';
import {
EditIconButton,
PlusIconButton,
} from '../../common/IconButtons/EditIconButton';
import DataProductsSelectList from '../DataProductsSelectList/DataProductsSelectList';
interface DataProductsContainerProps {
showHeader?: boolean;
hasPermission: boolean;
@ -77,11 +78,13 @@ const DataProductsContainer = ({
const autoCompleteFormSelectContainer = useMemo(() => {
return (
<DataProductsSelectForm
<DataProductsSelectList
open
defaultValue={(dataProducts ?? []).map(
(item) => item.fullyQualifiedName ?? ''
)}
fetchApi={fetchAPI}
fetchOptions={fetchAPI}
mode="multiple"
placeholder={t('label.data-product-plural')}
onCancel={handleCancel}
onSubmit={handleSave}
@ -139,6 +142,14 @@ const DataProductsContainer = ({
<Typography.Text className={classNames('text-sm font-medium')}>
{t('label.data-product-plural')}
</Typography.Text>
{showAddTagButton && (
<PlusIconButton
data-testid="add-data-product"
size="small"
title={t('label.add-data-product')}
onClick={handleAddClick}
/>
)}
{hasPermission && !isUndefined(activeDomain) && (
<Row gutter={12}>
{!isEmpty(dataProducts) && (
@ -159,41 +170,26 @@ const DataProductsContainer = ({
</Space>
)
);
}, [showHeader, dataProducts, hasPermission]);
const addTagButton = useMemo(
() =>
showAddTagButton ? (
<Col
className="m-t-xss"
data-testid="add-data-product"
onClick={handleAddClick}>
<TagsV1 startWith={TAG_START_WITH.PLUS} tag={TAG_CONSTANT} />
</Col>
) : null,
[showAddTagButton]
);
}, [showHeader, dataProducts, hasPermission, showAddTagButton]);
const cardProps = useMemo(() => {
return {
title: header,
};
}, [header]);
}, [header, showAddTagButton, isEditMode]);
return (
<ExpandableCard
cardProps={cardProps}
dataTestId="data-products-container"
isExpandDisabled={isEmpty(dataProducts)}>
{!isEditMode && (
{isEditMode ? (
autoCompleteFormSelectContainer
) : isEmpty(renderDataProducts) ? null : (
<Row data-testid="data-products-list">
<Col className="flex flex-wrap gap-2">
{addTagButton}
{renderDataProducts}
</Col>
<Col className="flex flex-wrap gap-2">{renderDataProducts}</Col>
</Row>
)}
{isEditMode && autoCompleteFormSelectContainer}
</ExpandableCard>
);
};

View File

@ -10,6 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { SelectProps } from 'antd';
import { DataProduct } from '../../../generated/entity/domains/dataProduct';
import { Paging } from '../../../generated/type/paging';
@ -18,12 +19,13 @@ export type DataProductSelectOption = {
value: DataProduct;
};
export interface DataProductsSelectListProps {
export interface DataProductsSelectListProps extends SelectProps {
mode?: 'multiple';
placeholder?: string;
debounceTimeout?: number;
defaultValue?: string[];
onChange?: (newValue: DataProduct[]) => void;
onSubmit?: (newValue: DataProduct[]) => Promise<void>;
onCancel?: () => void;
fetchOptions: (
search: string,
page: number

View File

@ -10,17 +10,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Select, Space, Tooltip, Typography } from 'antd';
import { Button, Select, Space, Tooltip, Typography } from 'antd';
import { AxiosError } from 'axios';
import { debounce } from 'lodash';
import React, {
forwardRef,
useCallback,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { DataProduct } from '../../../generated/entity/domains/dataProduct';
import { Paging } from '../../../generated/type/paging';
import { getEntityName } from '../../../utils/EntityUtils';
@ -30,160 +24,171 @@ import Loader from '../../common/Loader/Loader';
import {
DataProductSelectOption,
DataProductsSelectListProps,
DataProductsSelectRef,
} from './DataProductSelectList.interface';
const DataProductsSelectList = forwardRef<
DataProductsSelectRef,
DataProductsSelectListProps
>(
(
{
mode,
onChange,
fetchOptions,
debounceTimeout = 800,
defaultValue,
...props
}: DataProductsSelectListProps,
ref
) => {
const selectRef = useRef(null);
const [isLoading, setIsLoading] = useState(false);
const [hasContentLoading, setHasContentLoading] = useState(false);
const [options, setOptions] = useState<DataProductSelectOption[]>([]);
const [searchValue, setSearchValue] = useState<string>('');
const [paging, setPaging] = useState<Paging>({} as Paging);
const [currentPage, setCurrentPage] = useState(1);
const [selectedValue, setSelectedValue] = useState<DataProduct[]>([]);
const DataProductsSelectList = ({
mode,
onSubmit,
onCancel,
fetchOptions,
debounceTimeout = 800,
defaultValue,
...props
}: DataProductsSelectListProps) => {
const [isLoading, setIsLoading] = useState(false);
const [hasContentLoading, setHasContentLoading] = useState(false);
const [options, setOptions] = useState<DataProductSelectOption[]>([]);
const [searchValue, setSearchValue] = useState<string>('');
const [paging, setPaging] = useState<Paging>({} as Paging);
const [currentPage, setCurrentPage] = useState(1);
const [selectedValue, setSelectedValue] = useState<DataProduct[]>([]);
const { t } = useTranslation();
const [isSubmitLoading, setIsSubmitLoading] = useState(false);
const loadOptions = useCallback(
async (value: string) => {
setOptions([]);
setIsLoading(true);
const onSave = async () => {
setIsSubmitLoading(true);
try {
await onSubmit?.(selectedValue);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsSubmitLoading(false);
}
};
const loadOptions = useCallback(
async (value: string) => {
setOptions([]);
setIsLoading(true);
try {
const res = await fetchOptions(value, 1);
setOptions(res.data);
setPaging(res.paging);
setSearchValue(value);
setCurrentPage(1);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
},
[fetchOptions]
);
const debounceFetcher = useMemo(
() => debounce(loadOptions, debounceTimeout),
[loadOptions, debounceTimeout]
);
const selectOptions = useMemo(() => {
return options.map((item) => {
return {
label: item.label,
displayName: (
<Space className="w-full" direction="vertical" size={0}>
<Typography.Paragraph ellipsis className="text-grey-muted m-0 p-0">
{getEntityName(item.value.domain)}
</Typography.Paragraph>
<Typography.Text ellipsis>
{getEntityName(item.value)}
</Typography.Text>
</Space>
),
value: item.value.fullyQualifiedName,
};
});
}, [options]);
const onScroll = async (e: React.UIEvent<HTMLDivElement>) => {
const { currentTarget } = e;
if (
currentTarget.scrollTop + currentTarget.offsetHeight ===
currentTarget.scrollHeight
) {
if (options.length < paging.total) {
try {
const res = await fetchOptions(value, 1);
setOptions(res.data);
setHasContentLoading(true);
const res = await fetchOptions(searchValue, currentPage + 1);
setOptions((prev) => [...prev, ...res.data]);
setPaging(res.paging);
setSearchValue(value);
setCurrentPage(1);
setCurrentPage((prev) => prev + 1);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
},
[fetchOptions]
);
const debounceFetcher = useMemo(
() => debounce(loadOptions, debounceTimeout),
[loadOptions, debounceTimeout]
);
const selectOptions = useMemo(() => {
return options.map((item) => {
return {
label: item.label,
displayName: (
<Space className="w-full" direction="vertical" size={0}>
<Typography.Paragraph
ellipsis
className="text-grey-muted m-0 p-0">
{getEntityName(item.value.domain)}
</Typography.Paragraph>
<Typography.Text ellipsis>
{getEntityName(item.value)}
</Typography.Text>
</Space>
),
value: item.value.fullyQualifiedName,
};
});
}, [options]);
const onScroll = async (e: React.UIEvent<HTMLDivElement>) => {
const { currentTarget } = e;
if (
currentTarget.scrollTop + currentTarget.offsetHeight ===
currentTarget.scrollHeight
) {
if (options.length < paging.total) {
try {
setHasContentLoading(true);
const res = await fetchOptions(searchValue, currentPage + 1);
setOptions((prev) => [...prev, ...res.data]);
setPaging(res.paging);
setCurrentPage((prev) => prev + 1);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setHasContentLoading(false);
}
setHasContentLoading(false);
}
}
};
}
};
const dropdownRender = (menu: React.ReactElement) => (
<>
{menu}
{hasContentLoading ? <Loader size="small" /> : null}
</>
);
const dropdownRender = (menu: React.ReactElement) => (
<>
{menu}
{hasContentLoading ? <Loader size="small" /> : null}
<Space className="p-sm p-b-xss p-l-xs custom-dropdown-render" size={8}>
<Button
className="update-btn"
data-testid="saveAssociatedTag"
loading={isSubmitLoading}
size="small"
onClick={onSave}>
{t('label.update')}
</Button>
<Button
data-testid="cancelAssociatedTag"
size="small"
onClick={onCancel}>
{t('label.cancel')}
</Button>
</Space>
</>
);
const onSelectChange = (value: string[]) => {
const entityObj = value.reduce((result: DataProduct[], item) => {
const option = options.find((option) => option.label === item);
if (option) {
result.push(option.value);
}
const onSelectChange = (value: string[]) => {
const entityObj = value.reduce((result: DataProduct[], item) => {
const option = options.find((option) => option.label === item);
if (option) {
result.push(option.value);
}
return result;
}, []);
return result;
}, []);
setSelectedValue(entityObj as DataProduct[]);
};
setSelectedValue(entityObj as DataProduct[]);
};
useImperativeHandle(ref, () => ({
getSelectValue() {
return selectedValue;
},
}));
return (
<Select
autoFocus
showSearch
className="w-full"
data-testid="data-product-selector"
defaultValue={defaultValue}
dropdownRender={dropdownRender}
filterOption={false}
mode={mode}
notFoundContent={isLoading ? <Loader size="small" /> : null}
optionLabelProp="label"
ref={selectRef}
tagRender={tagRender}
onChange={onSelectChange}
onFocus={() => loadOptions('')}
onPopupScroll={onScroll}
onSearch={debounceFetcher}
{...props}>
{selectOptions.map(({ label, value, displayName }) => (
<Select.Option data-testid={`tag-${value}`} key={label} value={value}>
<Tooltip
destroyTooltipOnHide
mouseEnterDelay={1.5}
placement="leftTop"
title={label}
trigger="hover">
{displayName}
</Tooltip>
</Select.Option>
))}
</Select>
);
}
);
return (
<Select
autoFocus
showSearch
className="w-full"
data-testid="data-product-selector"
defaultValue={defaultValue}
dropdownRender={dropdownRender}
filterOption={false}
mode={mode}
notFoundContent={isLoading ? <Loader size="small" /> : null}
optionLabelProp="label"
tagRender={tagRender}
onChange={onSelectChange}
onFocus={() => loadOptions('')}
onPopupScroll={onScroll}
onSearch={debounceFetcher}
{...props}>
{selectOptions.map(({ label, value, displayName }) => (
<Select.Option data-testid={`tag-${value}`} key={label} value={value}>
<Tooltip
destroyTooltipOnHide
mouseEnterDelay={1.5}
placement="leftTop"
title={label}
trigger="hover">
{displayName}
</Tooltip>
</Select.Option>
))}
</Select>
);
};
export default DataProductsSelectList;

View File

@ -12,24 +12,24 @@
*/
import Icon from '@ant-design/icons';
import { Button, Col, Drawer, Row, Space, Tooltip, Typography } from 'antd';
import { Col, Drawer, Row, Space, Typography } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg';
import { ReactComponent as IconUser } from '../../../../assets/svg/user.svg';
import { DE_ACTIVE_COLOR } from '../../../../constants/constants';
import { EntityType } from '../../../../enums/entity.enum';
import { Query } from '../../../../generated/entity/data/query';
import { TagLabel } from '../../../../generated/type/tagLabel';
import { TagLabel, TagSource } from '../../../../generated/type/tagLabel';
import { getEntityName } from '../../../../utils/EntityUtils';
import { getUserPath } from '../../../../utils/RouterUtils';
import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1';
import ExpandableCard from '../../../common/ExpandableCard/ExpandableCard';
import { EditIconButton } from '../../../common/IconButtons/EditIconButton';
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';
import TagsContainerV2 from '../../../Tag/TagsContainerV2/TagsContainerV2';
import { TableQueryRightPanelProps } from './TableQueryRightPanel.interface';
const TableQueryRightPanel = ({
@ -79,66 +79,72 @@ const TableQueryRightPanel = ({
{isLoading ? (
<Loader />
) : (
<Row className="m-y-md p-x-md w-full" gutter={[16, 40]}>
<Row className="m-y-md p-x-md w-full" gutter={[16, 20]}>
<Col span={24}>
<Space className="relative" direction="vertical" size={4}>
<Space align="center" className="w-full" size={0}>
<Typography.Text className="right-panel-label">
{t('label.owner-plural')}
</Typography.Text>
<ExpandableCard
cardProps={{
title: (
<Space align="center" className="w-full" size={0}>
<Typography.Text className="right-panel-label">
{t('label.owner-plural')}
</Typography.Text>
{(EditAll || EditOwners) && (
<UserTeamSelectableList
hasPermission={EditAll || EditOwners}
multiple={{ user: true, team: false }}
owner={query.owners}
onUpdate={(updatedUsers) =>
handleUpdateOwner(updatedUsers)
}>
<Tooltip
title={t('label.edit-entity', {
entity: t('label.owner-lowercase-plural'),
})}>
<Button
className="cursor-pointer flex-center"
data-testid="edit-owner"
icon={<EditIcon color={DE_ACTIVE_COLOR} width="14px" />}
size="small"
type="text"
/>
</Tooltip>
</UserTeamSelectableList>
)}
</Space>
{(EditAll || EditOwners) && (
<UserTeamSelectableList
hasPermission={EditAll || EditOwners}
multiple={{ user: true, team: false }}
owner={query.owners}
onUpdate={(updatedUsers) =>
handleUpdateOwner(updatedUsers)
}>
<EditIconButton
data-testid="edit-owner"
size="small"
title={t('label.edit-entity', {
entity: t('label.owner-lowercase-plural'),
})}
/>
</UserTeamSelectableList>
)}
</Space>
),
}}>
<OwnerLabel hasPermission={false} owners={query.owners} />
</Space>
</ExpandableCard>
</Col>
<Col span={24}>
<Space direction="vertical" size={4}>
<DescriptionV1
description={query?.description || ''}
entityFullyQualifiedName={query?.fullyQualifiedName}
entityType={EntityType.QUERY}
hasEditAccess={EditDescription || EditAll}
showCommentsIcon={false}
onDescriptionUpdate={onDescriptionUpdate}
/>
</Space>
</Col>
<Col span={24}>
<TagsInput
editable={EditAll || EditTags}
tags={query?.tags || []}
onTagsUpdate={handleTagSelection}
<DescriptionV1
wrapInCard
className="w-full"
description={query?.description || ''}
entityFullyQualifiedName={query?.fullyQualifiedName}
entityType={EntityType.QUERY}
hasEditAccess={EditDescription || EditAll}
showCommentsIcon={false}
onDescriptionUpdate={onDescriptionUpdate}
/>
</Col>
<Col span={24}>
<Space className="m-b-md" direction="vertical" size={4}>
<Typography.Text
className="right-panel-label"
data-testid="users">
{t('label.user-plural')}
</Typography.Text>
<TagsContainerV2
newLook
permission={EditAll || EditTags}
selectedTags={query?.tags || []}
showTaskHandler={false}
tagType={TagSource.Classification}
onSelectionChange={handleTagSelection}
/>
</Col>
<Col span={24}>
<ExpandableCard
cardProps={{
title: (
<Typography.Text
className="right-panel-label"
data-testid="users">
{t('label.user-plural')}
</Typography.Text>
),
}}>
{query.users && query.users.length ? (
<Space wrap size={6}>
{query.users.map((user) => (
@ -161,15 +167,19 @@ const TableQueryRightPanel = ({
})}
</Typography.Paragraph>
)}
</Space>
</ExpandableCard>
</Col>
<Col span={24}>
<Space className="m-b-md" direction="vertical" size={4}>
<Typography.Text
className="right-panel-label"
data-testid="used-by">
{t('label.used-by')}
</Typography.Text>
<ExpandableCard
cardProps={{
title: (
<Typography.Text
className="right-panel-label"
data-testid="used-by">
{t('label.used-by')}
</Typography.Text>
),
}}>
{query.usedBy && query.usedBy.length ? (
<Space wrap size={6}>
{query.usedBy.map((user) => (
@ -186,7 +196,7 @@ const TableQueryRightPanel = ({
})}
</Typography.Paragraph>
)}
</Space>
</ExpandableCard>
</Col>
</Row>
)}

View File

@ -83,13 +83,13 @@ jest.mock('../../../common/EntityDescription/DescriptionV1', () => {
</div>
));
});
jest.mock('../../../TagsInput/TagsInput.component', () => {
return jest.fn().mockImplementation(({ onTagsUpdate }) => (
jest.mock('../../../Tag/TagsContainerV2/TagsContainerV2', () => {
return jest.fn().mockImplementation(({ onSelectionChange }) => (
<div>
TagsInput.component
<button
data-testid="update-tags-button"
onClick={() => onTagsUpdate(mockNewTag)}>
onClick={() => onSelectionChange(mockNewTag)}>
{' '}
Update Tags
</button>

View File

@ -51,7 +51,6 @@ const TableTags = <T extends TableUnion>({
entityType={entityType}
permission={hasTagEditAccess && !isReadOnly}
selectedTags={tags}
showHeader={false}
showInlineEditButton={showInlineEditTagButton}
sizeCap={TAG_LIST_SIZE}
tagType={type}

View File

@ -15,14 +15,15 @@ import classNames from 'classnames';
import { t } from 'i18next';
import { cloneDeep, includes, isEmpty, isEqual } from 'lodash';
import { default as React, useMemo } from 'react';
import { ReactComponent as PlusIcon } from '../../../assets/svg/plus-primary.svg';
import { TabSpecificField } from '../../../enums/entity.enum';
import { Domain } from '../../../generated/entity/domains/domain';
import { EntityReference } from '../../../generated/tests/testCase';
import { getOwnerVersionLabel } from '../../../utils/EntityVersionUtils';
import ExpandableCard from '../../common/ExpandableCard/ExpandableCard';
import { EditIconButton } from '../../common/IconButtons/EditIconButton';
import TagButton from '../../common/TagButton/TagButton.component';
import {
EditIconButton,
PlusIconButton,
} from '../../common/IconButtons/EditIconButton';
import { UserSelectableList } from '../../common/UserSelectableList/UserSelectableList.component';
import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider';
@ -69,53 +70,44 @@ export const DomainExpertWidget = () => {
data-testid="domain-expert-heading-name">
{t('label.expert-plural')}
</Typography.Text>
{editOwnerPermission && domain.experts && domain.experts.length > 0 && (
{editOwnerPermission && (
<UserSelectableList
hasPermission
popoverProps={{ placement: 'topLeft' }}
selectedUsers={domain.experts ?? []}
onUpdate={handleExpertsUpdate}>
<EditIconButton
newLook
data-testid="edit-expert-button"
size="small"
title={t('label.edit-entity', {
entity: t('label.expert-plural'),
})}
/>
{isEmpty(domain.experts) ? (
<PlusIconButton
data-testid="Add"
size="small"
title={t('label.add-entity', {
entity: t('label.expert-plural'),
})}
/>
) : (
<EditIconButton
newLook
data-testid="edit-expert-button"
size="small"
title={t('label.edit-entity', {
entity: t('label.expert-plural'),
})}
/>
)}
</UserSelectableList>
)}
</div>
);
const content = (
<>
<div>
{getOwnerVersionLabel(
domain,
isVersionView ?? false,
TabSpecificField.EXPERTS,
editAllPermission
)}
</div>
<div>
{editOwnerPermission && domain.experts?.length === 0 && (
<UserSelectableList
hasPermission={editOwnerPermission}
popoverProps={{ placement: 'topLeft' }}
selectedUsers={domain.experts ?? []}
onUpdate={handleExpertsUpdate}>
<TagButton
className="text-primary cursor-pointer"
icon={<PlusIcon height={16} name="plus" width={16} />}
label={t('label.add')}
tooltip=""
/>
</UserSelectableList>
)}
</div>
</>
const content = isEmpty(domain.experts) ? null : (
<div>
{getOwnerVersionLabel(
domain,
isVersionView ?? false,
TabSpecificField.EXPERTS,
editAllPermission
)}
</div>
);
return (

View File

@ -15,7 +15,6 @@ import { Space, Typography } from 'antd';
import { t } from 'i18next';
import { cloneDeep, isEmpty, isEqual } from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import { ReactComponent as PlusIcon } from '../../../../assets/svg/plus-primary.svg';
import { NO_DATA_PLACEHOLDER } from '../../../../constants/constants';
import { EntityField } from '../../../../constants/Feeds.constants';
import {
@ -30,8 +29,10 @@ import {
} from '../../../../utils/EntityVersionUtils';
import { renderReferenceElement } from '../../../../utils/GlossaryUtils';
import ExpandableCard from '../../../common/ExpandableCard/ExpandableCard';
import { EditIconButton } from '../../../common/IconButtons/EditIconButton';
import TagButton from '../../../common/TagButton/TagButton.component';
import {
EditIconButton,
PlusIconButton,
} from '../../../common/IconButtons/EditIconButton';
import { useGenericContext } from '../../../Customization/GenericProvider/GenericProvider';
import GlossaryTermReferencesModal from '../GlossaryTermReferencesModal.component';
@ -130,47 +131,49 @@ const GlossaryTermReferences = () => {
<Typography.Text className="text-sm font-medium">
{t('label.reference-plural')}
</Typography.Text>
{references.length > 0 && permissions.EditAll && (
<EditIconButton
newLook
data-testid="edit-button"
disabled={!permissions.EditAll}
size="small"
onClick={() => setIsViewMode(false)}
/>
)}
{permissions.EditAll &&
(isEmpty(references) ? (
<PlusIconButton
data-testid="term-references-add-button"
size="small"
title={t('label.add-entity', {
entity: t('label.reference-plural'),
})}
onClick={() => {
setIsViewMode(false);
}}
/>
) : (
<EditIconButton
newLook
data-testid="edit-button"
disabled={!permissions.EditAll}
size="small"
onClick={() => setIsViewMode(false)}
/>
))}
</Space>
);
return (
<ExpandableCard
cardProps={{
title: header,
}}
dataTestId="references-container"
isExpandDisabled={isEmpty(references)}>
{isVersionView ? (
getVersionReferenceElements()
) : (
<div className="d-flex flex-wrap">
{references.map((ref) => renderReferenceElement(ref))}
{permissions.EditAll && references.length === 0 && (
<TagButton
className="text-primary cursor-pointer"
dataTestId="term-references-add-button"
icon={<PlusIcon height={16} name="plus" width={16} />}
label={t('label.add')}
tooltip=""
onClick={() => {
setIsViewMode(false);
}}
/>
)}
{!permissions.EditAll && references.length === 0 && (
<div>{NO_DATA_PLACEHOLDER}</div>
)}
</div>
)}
<>
<ExpandableCard
cardProps={{
title: header,
}}
dataTestId="references-container"
isExpandDisabled={isEmpty(references)}>
{isVersionView ? (
getVersionReferenceElements()
) : !permissions.EditAll || !isEmpty(references) ? (
<div className="d-flex flex-wrap">
{references.map((ref) => renderReferenceElement(ref))}
{!permissions.EditAll && references.length === 0 && (
<div>{NO_DATA_PLACEHOLDER}</div>
)}
</div>
) : null}
</ExpandableCard>
<GlossaryTermReferencesModal
isVisible={!isViewMode}
@ -180,7 +183,7 @@ const GlossaryTermReferences = () => {
}}
onSave={handleReferencesSave}
/>
</ExpandableCard>
</>
);
};

View File

@ -16,7 +16,6 @@ import { Button, Select, Space, Typography } from 'antd';
import { t } from 'i18next';
import { cloneDeep, isEmpty, isEqual } from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import { ReactComponent as PlusIcon } from '../../../../assets/svg/plus-primary.svg';
import { NO_DATA_PLACEHOLDER } from '../../../../constants/constants';
import { EntityField } from '../../../../constants/Feeds.constants';
import { GlossaryTerm } from '../../../../generated/entity/data/glossaryTerm';
@ -27,7 +26,10 @@ import {
getDiffByFieldName,
} from '../../../../utils/EntityVersionUtils';
import ExpandableCard from '../../../common/ExpandableCard/ExpandableCard';
import { EditIconButton } from '../../../common/IconButtons/EditIconButton';
import {
EditIconButton,
PlusIconButton,
} from '../../../common/IconButtons/EditIconButton';
import TagButton from '../../../common/TagButton/TagButton.component';
import { useGenericContext } from '../../../Customization/GenericProvider/GenericProvider';
@ -42,32 +44,22 @@ const GlossaryTermSynonyms = () => {
permissions,
} = useGenericContext<GlossaryTerm>();
const getSynonyms = () => (
<div className="d-flex flex-wrap">
{synonyms.map((synonym) => (
<TagButton
className="glossary-synonym-tag"
key={synonym}
label={synonym}
/>
))}
{permissions.EditAll && synonyms.length === 0 && (
<TagButton
className="text-primary cursor-pointer"
dataTestId="synonym-add-button"
icon={<PlusIcon height={16} name="plus" width={16} />}
label={t('label.add')}
tooltip=""
onClick={() => {
setIsViewMode(false);
}}
/>
)}
{!permissions.EditAll && synonyms.length === 0 && (
<div>{NO_DATA_PLACEHOLDER}</div>
)}
</div>
);
const getSynonyms = () =>
!permissions.EditAll || !isEmpty(synonyms) ? (
<div className="d-flex flex-wrap">
{synonyms.map((synonym) => (
<TagButton
className="glossary-synonym-tag"
key={synonym}
label={synonym}
/>
))}
{!permissions.EditAll && synonyms.length === 0 && (
<div>{NO_DATA_PLACEHOLDER}</div>
)}
</div>
) : null;
const getSynonymsContainer = useCallback(() => {
if (!isVersionView) {
@ -174,17 +166,30 @@ const GlossaryTermSynonyms = () => {
<Typography.Text className="text-sm font-medium">
{t('label.synonym-plural')}
</Typography.Text>
{permissions.EditAll && synonyms.length > 0 && isViewMode && (
<EditIconButton
newLook
data-testid="edit-button"
size="small"
title={t('label.edit-entity', {
entity: t('label.synonym-plural'),
})}
onClick={() => setIsViewMode(false)}
/>
)}
{permissions.EditAll &&
isViewMode &&
(isEmpty(synonyms) ? (
<PlusIconButton
data-testid="synonym-add-button"
size="small"
title={t('label.add-entity', {
entity: t('label.synonym-plural'),
})}
onClick={() => {
setIsViewMode(false);
}}
/>
) : (
<EditIconButton
newLook
data-testid="edit-button"
size="small"
title={t('label.edit-entity', {
entity: t('label.synonym-plural'),
})}
onClick={() => setIsViewMode(false)}
/>
))}
</div>
);

View File

@ -18,7 +18,6 @@ import { isArray, isEmpty, isUndefined } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { ReactComponent as IconTerm } from '../../../../assets/svg/book.svg';
import { ReactComponent as PlusIcon } from '../../../../assets/svg/plus-primary.svg';
import TagSelectForm from '../../../../components/Tag/TagsSelectForm/TagsSelectForm.component';
import { NO_DATA_PLACEHOLDER } from '../../../../constants/constants';
import { EntityField } from '../../../../constants/Feeds.constants';
@ -41,7 +40,10 @@ import { VersionStatus } from '../../../../utils/EntityVersionUtils.interface';
import { getGlossaryPath } from '../../../../utils/RouterUtils';
import { SelectOption } from '../../../common/AsyncSelectList/AsyncSelectList.interface';
import ExpandableCard from '../../../common/ExpandableCard/ExpandableCard';
import { EditIconButton } from '../../../common/IconButtons/EditIconButton';
import {
EditIconButton,
PlusIconButton,
} from '../../../common/IconButtons/EditIconButton';
import TagButton from '../../../common/TagButton/TagButton.component';
import { useGenericContext } from '../../../Customization/GenericProvider/GenericProvider';
@ -193,21 +195,8 @@ const RelatedTerms = () => {
() =>
isVersionView ? (
getVersionRelatedTerms()
) : (
) : !permissions.EditAll || !isEmpty(selectedOption) ? (
<div className="d-flex flex-wrap">
{permissions.EditAll && selectedOption.length === 0 && (
<TagButton
className="text-primary cursor-pointer"
dataTestId="related-term-add-button"
icon={<PlusIcon height={16} name="plus" width={16} />}
label={t('label.add')}
tooltip=""
onClick={() => {
setIsIconVisible(false);
}}
/>
)}
{selectedOption.map((entity: EntityReference) =>
getRelatedTermElement(entity)
)}
@ -216,7 +205,7 @@ const RelatedTerms = () => {
<div>{NO_DATA_PLACEHOLDER}</div>
)}
</div>
),
) : null,
[
permissions,
selectedOption,
@ -231,17 +220,29 @@ const RelatedTerms = () => {
<Typography.Text className="text-sm font-medium">
{t('label.related-term-plural')}
</Typography.Text>
{permissions.EditAll && selectedOption.length > 0 && (
<EditIconButton
newLook
data-testid="edit-button"
size="small"
title={t('label.edit-entity', {
entity: t('label.related-term-plural'),
})}
onClick={() => setIsIconVisible(false)}
/>
)}
{permissions.EditAll &&
(isEmpty(selectedOption) ? (
<PlusIconButton
data-testid="related-term-add-button"
size="small"
title={t('label.add-entity', {
entity: t('label.related-term-plural'),
})}
onClick={() => {
setIsIconVisible(false);
}}
/>
) : (
<EditIconButton
newLook
data-testid="edit-button"
size="small"
title={t('label.edit-entity', {
entity: t('label.related-term-plural'),
})}
onClick={() => setIsIconVisible(false)}
/>
))}
</div>
);

View File

@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Col, Row, Space, Typography } from 'antd';
import { Button, Space, Typography } from 'antd';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
@ -18,7 +18,6 @@ import React, { FC, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { NO_DATA_PLACEHOLDER } from '../../../constants/constants';
import { TAG_CONSTANT, TAG_START_WITH } from '../../../constants/Tag.constants';
import { Metric } from '../../../generated/entity/data/metric';
import { EntityReference } from '../../../generated/type/entityReference';
import entityUtilClassBase from '../../../utils/EntityUtilClassBase';
@ -26,10 +25,12 @@ import { getEntityName } from '../../../utils/EntityUtils';
import { getEntityIcon } from '../../../utils/TableUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import ExpandableCard from '../../common/ExpandableCard/ExpandableCard';
import { EditIconButton } from '../../common/IconButtons/EditIconButton';
import {
EditIconButton,
PlusIconButton,
} from '../../common/IconButtons/EditIconButton';
import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider';
import { DataAssetOption } from '../../DataAssets/DataAssetAsyncSelectList/DataAssetAsyncSelectList.interface';
import TagsV1 from '../../Tag/TagsV1/TagsV1.component';
import './related-metrics.less';
import { RelatedMetricsForm } from './RelatedMetricsForm';
@ -160,59 +161,51 @@ const RelatedMetrics: FC<RelatedMetricsProps> = ({
{t('label.related-metric-plural')}
</Typography.Text>
{!isEdit &&
!isEmpty(relatedMetrics) &&
permissions.EditAll &&
!metricDetails.deleted && (
!metricDetails.deleted &&
(isEmpty(relatedMetrics) ? (
<PlusIconButton
data-testid="add-related-metrics-container"
size="small"
title={t('label.add-entity', {
entity: t('label.related-metric-plural'),
})}
onClick={() => setIsEdit(true)}
/>
) : (
<EditIconButton
newLook
data-testid="edit-related-metrics"
size="small"
title={t('label.edit-entity', {
entity: t('label.related-metric-plural'),
})}
onClick={() => setIsEdit(true)}
/>
)}
))}
</Space>
);
const content = (
<>
{isEmpty(relatedMetrics) &&
!isEdit &&
permissions.EditAll &&
!metricDetails.deleted && (
<Col
className="m-t-xss"
data-testid="add-related-metrics-container"
onClick={() => setIsEdit(true)}>
<TagsV1 startWith={TAG_START_WITH.PLUS} tag={TAG_CONSTANT} />
</Col>
)}
<Col span={24}>
{isEdit ? (
<RelatedMetricsForm
defaultValue={defaultValue}
initialOptions={initialOptions}
metricFqn={metricDetails.fullyQualifiedName ?? ''}
onCancel={() => setIsEdit(false)}
onSubmit={handleRelatedMetricUpdate}
/>
) : (
<>
{isEmpty(relatedMetrics) &&
(metricDetails.deleted || isInSummaryPanel) ? (
<Typography.Text>{NO_DATA_PLACEHOLDER}</Typography.Text>
) : (
<div
className="metric-entity-list-body"
data-testid="metric-entity-list-body">
{getRelatedMetricListing(visibleRelatedMetrics)}
{isShowMore && getRelatedMetricListing(hiddenRelatedMetrics)}
{!isEmpty(hiddenRelatedMetrics) && showMoreLessElement}
</div>
)}
</>
)}
</Col>
</>
const content = isEdit ? (
<RelatedMetricsForm
defaultValue={defaultValue}
initialOptions={initialOptions}
metricFqn={metricDetails.fullyQualifiedName ?? ''}
onCancel={() => setIsEdit(false)}
onSubmit={handleRelatedMetricUpdate}
/>
) : isEmpty(relatedMetrics) && (metricDetails.deleted || isInSummaryPanel) ? (
<Typography.Text>{NO_DATA_PLACEHOLDER}</Typography.Text>
) : (
!isEmpty(relatedMetrics) && (
<div
className="metric-entity-list-body"
data-testid="metric-entity-list-body">
{getRelatedMetricListing(visibleRelatedMetrics)}
{isShowMore && getRelatedMetricListing(hiddenRelatedMetrics)}
{!isEmpty(hiddenRelatedMetrics) && showMoreLessElement}
</div>
)
);
return (
@ -221,7 +214,7 @@ const RelatedMetrics: FC<RelatedMetricsProps> = ({
title: header,
}}
isExpandDisabled={isEmpty(relatedMetrics)}>
<Row gutter={[0, 8]}>{content}</Row>
{content}
</ExpandableCard>
);
};

View File

@ -26,7 +26,6 @@ export type TagsContainerV2Props = {
columnData?: {
fqn: string;
};
showHeader?: boolean;
showBottomEditButton?: boolean;
showInlineEditButton?: boolean;
children?: ReactElement;

View File

@ -43,6 +43,7 @@ import ExpandableCard from '../../common/ExpandableCard/ExpandableCard';
import {
CommentIconButton,
EditIconButton,
PlusIconButton,
RequestIconButton,
} from '../../common/IconButtons/EditIconButton';
import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider';
@ -65,7 +66,6 @@ const TagsContainerV2 = ({
tagType,
displayType,
layoutType,
showHeader = true,
showBottomEditButton,
showInlineEditButton,
columnData,
@ -173,29 +173,34 @@ const TagsContainerV2 = ({
const addTagButton = useMemo(
() =>
showAddTagButton ? (
<Col className="m-t-xss" onClick={handleAddClick}>
<TagsV1
startWith={TAG_START_WITH.PLUS}
tag={isGlossaryType ? GLOSSARY_CONSTANT : TAG_CONSTANT}
tagType={tagType}
/>
</Col>
<PlusIconButton
className="m-t-xss"
data-testid="add-tag"
size="small"
title={t('label.add-entity', {
entity: isGlossaryType
? t('label.glossary-term')
: t('label.tag-plural'),
})}
onClick={handleAddClick}
/>
) : null,
[showAddTagButton]
[showAddTagButton, handleAddClick, t, isGlossaryType]
);
const renderTags = useMemo(
() => (
<Col span={24}>
<TagsViewer
displayType={displayType}
showNoDataPlaceholder={showNoDataPlaceholder}
sizeCap={sizeCap}
tagType={tagType}
tags={tags?.[tagType] ?? []}
/>
</Col>
),
() =>
isEmpty(tags?.[tagType]) && !showNoDataPlaceholder ? null : (
<Col span={24}>
<TagsViewer
displayType={displayType}
showNoDataPlaceholder={showNoDataPlaceholder}
sizeCap={sizeCap}
tagType={tagType}
tags={tags?.[tagType] ?? []}
/>
</Col>
),
[displayType, showNoDataPlaceholder, tags?.[tagType], layoutType]
);
@ -267,46 +272,43 @@ const TagsContainerV2 = ({
const header = useMemo(() => {
return (
showHeader && (
<Space>
<Typography.Text
className={classNames({
'text-sm font-medium': newLook,
'right-panel-label': !newLook,
})}>
{isGlossaryType ? t('label.glossary-term') : t('label.tag-plural')}
</Typography.Text>
{permission && (
<>
{!isEmpty(tags?.[tagType]) && !isEditTags && (
<EditIconButton
data-testid="edit-button"
newLook={newLook}
size="small"
title={t('label.edit-entity', {
entity:
tagType === TagSource.Classification
? t('label.tag-plural')
: t('label.glossary-term'),
})}
onClick={handleAddClick}
/>
)}
{showTaskHandler && (
<>
{tagType === TagSource.Classification && requestTagElement}
{conversationThreadElement}
</>
)}
</>
)}
</Space>
)
<Space>
<Typography.Text
className={classNames({
'text-sm font-medium': newLook,
'right-panel-label': !newLook,
})}>
{isGlossaryType ? t('label.glossary-term') : t('label.tag-plural')}
</Typography.Text>
{permission && (
<>
{addTagButton ?? (
<EditIconButton
data-testid="edit-button"
newLook={newLook}
size="small"
title={t('label.edit-entity', {
entity:
tagType === TagSource.Classification
? t('label.tag-plural')
: t('label.glossary-term'),
})}
onClick={handleAddClick}
/>
)}
{showTaskHandler && (
<>
{tagType === TagSource.Classification && requestTagElement}
{conversationThreadElement}
</>
)}
</>
)}
</Space>
);
}, [
tags,
tagType,
showHeader,
isEditTags,
permission,
showTaskHandler,
@ -372,13 +374,21 @@ const TagsContainerV2 = ({
} else {
return isHoriZontalLayout ? (
horizontalLayout
) : (
) : showInlineEditButton || !isEmpty(renderTags) || !newLook ? (
<Row data-testid="entity-tags">
{addTagButton}
{showAddTagButton && (
<Col className="m-t-xss" onClick={handleAddClick}>
<TagsV1
startWith={TAG_START_WITH.PLUS}
tag={isGlossaryType ? GLOSSARY_CONSTANT : TAG_CONSTANT}
tagType={tagType}
/>
</Col>
)}
{renderTags}
{showInlineEditButton && <Col>{editTagButton}</Col>}
{showInlineEditButton ? <Col>{editTagButton}</Col> : null}
</Row>
);
) : null;
}
}, [
isEditTags,
@ -429,17 +439,7 @@ const TagsContainerV2 = ({
}}
dataTestId={isGlossaryType ? 'glossary-container' : 'tags-container'}
isExpandDisabled={isEmpty(tags?.[tagType])}>
{suggestionDataRender ?? (
<>
{tagBody}
{(children || showBottomEditButton) && (
<Space align="baseline" className="m-t-xs w-full" size="middle">
{showBottomEditButton && !showInlineEditButton && editTagButton}
{children}
</Space>
)}
</>
)}
{suggestionDataRender ?? tagBody}
</ExpandableCard>
);
}
@ -448,8 +448,6 @@ const TagsContainerV2 = ({
<div
className="w-full tags-container"
data-testid={isGlossaryType ? 'glossary-container' : 'tags-container'}>
{header}
{suggestionDataRender ?? (
<>
{tagBody}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2024 Collate.
* Copyright 2025 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
@ -10,136 +10,129 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { act, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { LabelType, State, TagSource } from '../../generated/type/tagLabel';
import {
LabelType,
State,
TagLabel,
TagSource,
} from '../../generated/type/tagLabel';
import TagsContainerV2 from '../Tag/TagsContainerV2/TagsContainerV2';
import TagsInput from './TagsInput.component';
const mockOnTagsUpdate = jest.fn();
jest.mock('../../components/Tag/TagsContainerV2/TagsContainerV2', () => {
return jest
.fn()
.mockImplementation(() => (
<div data-testid="tags-container">Mocked TagsContainerV2</div>
));
});
const tags = [
{
tagFQN: 'tag1',
displayName: 'Tag 1',
labelType: LabelType.Automated,
source: TagSource.Classification,
state: State.Confirmed,
},
{
tagFQN: 'tag2',
displayName: 'Tag 2',
description: 'This is a sample tag description.',
labelType: LabelType.Derived,
source: TagSource.Glossary,
state: State.Suggested,
},
];
describe('TagsInput Component', () => {
const mockTags: TagLabel[] = [
{
tagFQN: 'test.tag1',
source: TagSource.Classification,
labelType: LabelType.Manual,
state: State.Confirmed,
},
{
tagFQN: 'test.tag2',
source: TagSource.Classification,
labelType: LabelType.Manual,
state: State.Confirmed,
},
];
describe('TagsInput', () => {
it('should render TagsInput along with tagsViewer in version view', async () => {
await act(async () => {
render(
<TagsInput
isVersionView
editable={false}
tags={tags}
onTagsUpdate={mockOnTagsUpdate}
/>,
{
wrapper: MemoryRouter,
}
);
});
const mockOnTagsUpdate = jest.fn();
expect(
await screen.findByTestId('tags-input-container')
).toBeInTheDocument();
expect(await screen.findByText('label.tag-plural')).toBeInTheDocument();
expect(await screen.findByText('Tag 1')).toBeInTheDocument();
expect(await screen.findByText('Tag 2')).toBeInTheDocument();
it('renders without crashing', () => {
render(
<TagsInput editable tags={mockTags} onTagsUpdate={mockOnTagsUpdate} />,
{ wrapper: MemoryRouter }
);
expect(screen.getByTestId('tags-input-container')).toBeInTheDocument();
});
it('should render tags container when not in in version view', async () => {
await act(async () => {
render(
<TagsInput
editable={false}
tags={tags}
onTagsUpdate={mockOnTagsUpdate}
/>,
{
wrapper: MemoryRouter,
}
);
});
it('renders in version view mode', () => {
render(
<TagsInput
editable
isVersionView
tags={mockTags}
onTagsUpdate={mockOnTagsUpdate}
/>,
{ wrapper: MemoryRouter }
);
expect(
await screen.findByTestId('tags-input-container')
).toBeInTheDocument();
expect(await screen.findByTestId('tags-container')).toBeInTheDocument();
expect(await screen.findByText('label.tag-plural')).toBeInTheDocument();
expect(await screen.findByText('Tag 1')).toBeInTheDocument();
expect(screen.getByText('label.tag-plural')).toBeInTheDocument();
});
it('should render edit button when no editable', async () => {
await act(async () => {
render(
<TagsInput editable tags={tags} onTagsUpdate={mockOnTagsUpdate} />,
{
wrapper: MemoryRouter,
}
);
});
it('renders tags in version view mode', () => {
render(
<TagsInput
editable
isVersionView
tags={mockTags}
onTagsUpdate={mockOnTagsUpdate}
/>,
{ wrapper: MemoryRouter }
);
expect(await screen.findByTestId('edit-button')).toBeInTheDocument();
});
it('should not render edit button when no editable', async () => {
await act(async () => {
render(
<TagsInput
editable={false}
tags={tags}
onTagsUpdate={mockOnTagsUpdate}
/>,
{
wrapper: MemoryRouter,
}
);
});
expect(await screen.queryByTestId('edit-button')).not.toBeInTheDocument();
});
it('should not render tags if tags is empty', async () => {
await act(async () => {
render(
<TagsInput
editable={false}
tags={[]}
onTagsUpdate={mockOnTagsUpdate}
/>,
{
wrapper: MemoryRouter,
}
);
expect(await screen.findByTestId('tags-container')).toBeInTheDocument();
expect(await screen.findByTestId('entity-tags')).toBeInTheDocument();
expect(await screen.findByText('--')).toBeInTheDocument();
mockTags.forEach((tag) => {
expect(screen.getByText(tag.tagFQN)).toBeInTheDocument();
});
});
it('should render add tags if tags is empty and has permission', async () => {
await act(async () => {
render(<TagsInput editable tags={[]} onTagsUpdate={mockOnTagsUpdate} />, {
wrapper: MemoryRouter,
});
it('renders TagsContainerV2 when not in version view', () => {
render(
<TagsInput
editable
isVersionView={false}
tags={mockTags}
onTagsUpdate={mockOnTagsUpdate}
/>,
{ wrapper: MemoryRouter }
);
expect(await screen.findByTestId('entity-tags')).toBeInTheDocument();
expect(await screen.findByTestId('add-tag')).toBeInTheDocument();
// Verify TagsContainerV2 is rendered
expect(screen.getByTestId('tags-container')).toBeInTheDocument();
});
it('handles empty tags array', () => {
render(<TagsInput editable tags={[]} onTagsUpdate={mockOnTagsUpdate} />, {
wrapper: MemoryRouter,
});
expect(screen.getByTestId('tags-input-container')).toBeInTheDocument();
});
it('disables tag editing when editable is false', () => {
render(
<TagsInput
editable={false}
tags={mockTags}
onTagsUpdate={mockOnTagsUpdate}
/>,
{ wrapper: MemoryRouter }
);
expect(TagsContainerV2).toHaveBeenCalledWith(
expect.objectContaining({
permission: false,
}),
{}
);
});
it('handles undefined tags prop', () => {
render(<TagsInput editable onTagsUpdate={mockOnTagsUpdate} />, {
wrapper: MemoryRouter,
});
expect(screen.getByTestId('tags-input-container')).toBeInTheDocument();
});
});

View File

@ -39,10 +39,6 @@ describe('ExpandableCard', () => {
expect(screen.getByText('Test Card')).toBeInTheDocument();
expect(screen.getByTestId('test-content')).toBeInTheDocument();
expect(screen.getByRole('button')).toHaveAttribute(
'title',
'label.collapse'
);
});
it('renders with custom data-testid', () => {
@ -104,7 +100,6 @@ describe('ExpandableCard', () => {
const expandButton = screen.getByRole('button');
// Initial state (collapsed)
expect(expandButton).toHaveAttribute('title', 'label.collapse');
expect(expandButton.closest('.ant-card')).toHaveClass('expanded');
// Click to collapse
@ -112,7 +107,6 @@ describe('ExpandableCard', () => {
fireEvent.click(expandButton);
});
expect(expandButton).toHaveAttribute('title', 'label.expand');
expect(expandButton.closest('.ant-card')).not.toHaveClass('collapsed');
// Click to expand again
@ -120,7 +114,6 @@ describe('ExpandableCard', () => {
fireEvent.click(expandButton);
});
expect(expandButton).toHaveAttribute('title', 'label.collapse');
expect(expandButton.closest('.ant-card')).toHaveClass('expanded');
});
@ -239,8 +232,9 @@ describe('ExpandableCard', () => {
fireEvent.click(expandButton);
});
// Should not throw any errors
expect(expandButton).toHaveAttribute('title', 'label.expand');
const card = screen.getByRole('button').closest('.ant-card');
expect(card).not.toHaveClass('expanded');
});
it('works with minimal cardProps', () => {

View File

@ -44,6 +44,10 @@ const ExpandableCard = ({
return (
<Card
bodyStyle={{
// This will prevent the card body from having padding when there is no content
padding: children ? undefined : '0px',
}}
className={classNames(
'new-header-border-card w-full',
{

View File

@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Icon from '@ant-design/icons';
import Icon, { PlusOutlined } from '@ant-design/icons';
import { Button, ButtonProps, Tooltip } from 'antd';
import classNames from 'classnames';
import React from 'react';
@ -129,17 +129,42 @@ export const AlignRightIconButton = ({
export const CardExpandCollapseIconButton = ({
title,
className,
size,
disabled,
...props
}: IconButtonPropsInternal) => {
return (
const button = (
<Button
className={classNames('bordered', className)}
disabled={disabled}
icon={<CardExpandCollapseIcon />}
size={size}
title={title}
type="text"
{...props}
/>
);
return (
<Tooltip title={title}>
{/* Adding span to fix the issue with className is not being applied for disabled button
Refer this comment for more details https://github.com/ant-design/ant-design/issues/21404#issuecomment-586800984 */}
{disabled ? <span className={className}>{button}</span> : button}
</Tooltip>
);
};
export const PlusIconButton = ({
title,
className,
size,
...props
}: IconButtonPropsInternal) => {
return (
<Tooltip title={title}>
<Button
className={classNames('bordered', className)}
icon={<PlusOutlined />}
size={size}
{...props}
/>
</Tooltip>
);
};

View File

@ -50,7 +50,7 @@ export const FrequentlyJoinedTables = () => {
const content = joinedTables.map((table) => (
<Space
className="w-full frequently-joint-data justify-between"
className="w-full frequently-joint-data justify-between m-t-xss"
data-testid="related-tables-data"
key={table.name}
size={4}>

View File

@ -16,10 +16,11 @@ import { isEmpty, map } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { ReactComponent as PlusIcon } from '../../../assets/svg/plus-primary.svg';
import ExpandableCard from '../../../components/common/ExpandableCard/ExpandableCard';
import { EditIconButton } from '../../../components/common/IconButtons/EditIconButton';
import TagButton from '../../../components/common/TagButton/TagButton.component';
import {
EditIconButton,
PlusIconButton,
} from '../../../components/common/IconButtons/EditIconButton';
import { useGenericContext } from '../../../components/Customization/GenericProvider/GenericProvider';
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import { EntityType, FqnPart } from '../../../enums/entity.enum';
@ -63,30 +64,29 @@ const TableConstraints = () => {
{t('label.table-constraints')}
</Typography.Text>
{hasPermission && !isEmpty(data?.tableConstraints) && (
<EditIconButton
newLook
data-testid="edit-table-constraint-button"
size="small"
onClick={handleOpenEditConstraintModal}
/>
)}
{hasPermission &&
(isEmpty(data?.tableConstraints) ? (
<PlusIconButton
data-testid="table-constraints-add-button"
size="small"
title={t('label.add-entity', {
entity: t('label.table-constraints'),
})}
onClick={handleOpenEditConstraintModal}
/>
) : (
<EditIconButton
newLook
data-testid="edit-table-constraint-button"
size="small"
onClick={handleOpenEditConstraintModal}
/>
))}
</Space>
);
const content = (
const content = isEmpty(data?.tableConstraints) ? null : (
<Space className="w-full new-header-border-card" direction="vertical">
{hasPermission && isEmpty(data?.tableConstraints) && (
<TagButton
className="text-primary cursor-pointer"
dataTestId="table-constraints-add-button"
icon={<PlusIcon height={16} name="plus" width={16} />}
label={t('label.add')}
tooltip=""
onClick={handleOpenEditConstraintModal}
/>
)}
{data?.tableConstraints?.map(
({ constraintType, columns, referredColumns }) => {
if (constraintType === ConstraintType.PrimaryKey) {
@ -163,6 +163,18 @@ const TableConstraints = () => {
return null;
}
)}
</Space>
);
return (
<>
<ExpandableCard
cardProps={{
title: header,
}}
isExpandDisabled={isEmpty(data?.tableConstraints)}>
{content}
</ExpandableCard>
{isModalOpen && (
<TableConstraintsModal
constraint={data?.tableConstraints}
@ -171,17 +183,7 @@ const TableConstraints = () => {
onSave={handleSubmit}
/>
)}
</Space>
);
return (
<ExpandableCard
cardProps={{
title: header,
}}
isExpandDisabled={isEmpty(data?.tableConstraints)}>
{content}
</ExpandableCard>
</>
);
};