mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-07-23 09:22:18 +00:00
chore(client): new UI for the tier-card (#12536)
* chore(client): tier-card ui update * minor change * test(client): for the new UI of tier-card * address comments * fix UI * replace some css with utility classes * add language locales for word Clear * some css and test fix * fix(client): cypress test for add and remove tier
This commit is contained in:
parent
60c9477d18
commit
e32b763e15
@ -62,16 +62,13 @@ const addRemoveOwner = () => {
|
||||
const addRemoveTier = () => {
|
||||
cy.get('[data-testid="edit-tier"]').click();
|
||||
cy.get('[data-testid="card-list"]').first().should('be.visible').as('tier1');
|
||||
cy.get('@tier1')
|
||||
.find('[data-testid="icon"] > [data-testid="select-tier-button"]')
|
||||
.click();
|
||||
cy.get('@tier1').find('[data-testid="radio-btn"]').click();
|
||||
verifyResponseStatusCode('@patchOwner', 200);
|
||||
cy.clickOutside();
|
||||
cy.get('[data-testid="Tier"]').should('contain', TIER);
|
||||
|
||||
cy.get('[data-testid="edit-tier"]').click();
|
||||
cy.get('[data-testid="card-list"]').first().should('be.visible').as('tier1');
|
||||
cy.get('@tier1').find('[data-testid="remove-tier"]').click();
|
||||
cy.get('[data-testid="clear-tier"]').should('be.visible').click();
|
||||
|
||||
verifyResponseStatusCode('@patchOwner', 200);
|
||||
cy.get('[data-testid="Tier"]').should('contain', 'No Tier');
|
||||
|
@ -11,7 +11,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { findByTestId, render } from '@testing-library/react';
|
||||
import {
|
||||
findByTestId,
|
||||
getAllByTestId,
|
||||
getByTestId,
|
||||
getByText,
|
||||
render,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import TierCard from './TierCard';
|
||||
|
||||
@ -31,10 +39,19 @@ const mockTierData = [
|
||||
},
|
||||
];
|
||||
|
||||
const mockGetTags = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({ data: mockTierData }));
|
||||
const mockOnUpdate = jest.fn();
|
||||
const mockShowErrorToast = jest.fn();
|
||||
const mockProps = {
|
||||
currentTier: 'currentTier',
|
||||
updateTier: mockOnUpdate,
|
||||
children: <div>Child</div>,
|
||||
};
|
||||
|
||||
jest.mock('rest/tagAPI', () => ({
|
||||
getTags: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({ data: mockTierData })),
|
||||
getTags: jest.fn().mockImplementation(() => mockGetTags()),
|
||||
}));
|
||||
|
||||
jest.mock('../../Loader/Loader', () => {
|
||||
@ -42,7 +59,7 @@ jest.mock('../../Loader/Loader', () => {
|
||||
});
|
||||
|
||||
jest.mock('../../../utils/ToastUtils', () => {
|
||||
return jest.fn().mockReturnValue(<div>showErrorToast</div>);
|
||||
return jest.fn().mockImplementation(() => mockShowErrorToast());
|
||||
});
|
||||
|
||||
// Mock Antd components
|
||||
@ -51,19 +68,58 @@ jest.mock('antd', () => ({
|
||||
|
||||
Popover: jest
|
||||
.fn()
|
||||
.mockImplementation(({ content }) => (
|
||||
<div data-testid="tier-card-container">{content}</div>
|
||||
)),
|
||||
.mockImplementation(({ content, onOpenChange, children }) => {
|
||||
onOpenChange(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
{content}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}),
|
||||
}));
|
||||
|
||||
const MockOnUpdate = jest.fn();
|
||||
jest.mock('../rich-text-editor/RichTextEditorPreviewer', () => {
|
||||
return jest.fn().mockReturnValue(<div>RichTextEditorPreviewer</div>);
|
||||
});
|
||||
|
||||
describe('Test TierCard Component', () => {
|
||||
it('Component should have card', async () => {
|
||||
const { container } = render(
|
||||
<TierCard currentTier="" updateTier={MockOnUpdate} />
|
||||
);
|
||||
const { container } = render(<TierCard {...mockProps} />);
|
||||
|
||||
await expect(getByText(container, 'Loader')).toBeInTheDocument();
|
||||
|
||||
expect(mockGetTags).toHaveBeenCalled();
|
||||
|
||||
expect(await findByTestId(container, 'cards')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call the mockOnUpdate when click on radio button', async () => {
|
||||
const { container } = render(<TierCard {...mockProps} />);
|
||||
|
||||
await waitForElementToBeRemoved(() => getByText(container, 'Loader'));
|
||||
|
||||
const radioBtns = getAllByTestId(container, 'radio-btn');
|
||||
|
||||
expect(radioBtns).toHaveLength(1);
|
||||
|
||||
userEvent.click(radioBtns[0]);
|
||||
|
||||
expect(mockOnUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call the mockOnUpdate when click on Clear button', async () => {
|
||||
const { container } = render(<TierCard {...mockProps} />);
|
||||
|
||||
await waitForElementToBeRemoved(() => getByText(container, 'Loader'));
|
||||
|
||||
const clearTier = getByTestId(container, 'clear-tier');
|
||||
|
||||
expect(clearTier).toBeInTheDocument();
|
||||
|
||||
userEvent.click(clearTier);
|
||||
|
||||
expect(mockOnUpdate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -11,24 +11,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Icon from '@ant-design/icons/lib/components/Icon';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Collapse,
|
||||
Popover,
|
||||
Row,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Space,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { ReactComponent as IconRemove } from 'assets/svg/ic-remove.svg';
|
||||
import { AxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import Loader from 'components/Loader/Loader';
|
||||
import { t } from 'i18next';
|
||||
import { LoadingState } from 'Models';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { getTags } from 'rest/tagAPI';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
@ -39,198 +33,111 @@ import { CardWithListItems, TierCardProps } from './TierCard.interface';
|
||||
const { Panel } = Collapse;
|
||||
const TierCard = ({ currentTier, updateTier, children }: TierCardProps) => {
|
||||
const [tierData, setTierData] = useState<Array<CardWithListItems>>([]);
|
||||
const [activeTier, setActiveTier] = useState(currentTier);
|
||||
const [statusTier, setStatusTier] = useState<LoadingState>('initial');
|
||||
const [isLoadingTierData, setIsLoadingTierData] = useState<boolean>(false);
|
||||
|
||||
const handleCardSelection = (cardId: string) => {
|
||||
setActiveTier(cardId);
|
||||
};
|
||||
|
||||
const setInitialTierLoadingState = () => {
|
||||
setStatusTier('initial');
|
||||
};
|
||||
|
||||
const getTierData = () => {
|
||||
const getTierData = async () => {
|
||||
setIsLoadingTierData(true);
|
||||
getTags({
|
||||
parent: 'Tier',
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data) {
|
||||
const tierData: CardWithListItems[] =
|
||||
data.map((tier: { name: string; description: string }) => ({
|
||||
id: `Tier${FQN_SEPARATOR_CHAR}${tier.name}`,
|
||||
title: tier.name,
|
||||
description: tier.description.substring(
|
||||
0,
|
||||
tier.description.indexOf('\n\n')
|
||||
),
|
||||
data: tier.description.substring(
|
||||
tier.description.indexOf('\n\n') + 1
|
||||
),
|
||||
})) ?? [];
|
||||
setTierData(tierData);
|
||||
} else {
|
||||
setTierData([]);
|
||||
}
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
showErrorToast(
|
||||
err,
|
||||
t('server.entity-fetch-error', {
|
||||
entity: t('label.tier-plural-lowercase'),
|
||||
})
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoadingTierData(false);
|
||||
try {
|
||||
const { data } = await getTags({
|
||||
parent: 'Tier',
|
||||
});
|
||||
};
|
||||
|
||||
const prepareTier = (updatedTier: string) => {
|
||||
return updatedTier !== currentTier ? updatedTier : undefined;
|
||||
};
|
||||
|
||||
const handleTierSave = (updatedTier: string) => {
|
||||
setStatusTier('waiting');
|
||||
|
||||
const newTier = prepareTier(updatedTier);
|
||||
updateTier?.(newTier as string);
|
||||
};
|
||||
|
||||
const getTierSelectButton = (tier: string) => {
|
||||
switch (statusTier) {
|
||||
case 'waiting':
|
||||
return (
|
||||
<Loader
|
||||
className="d-inline-block"
|
||||
size="small"
|
||||
style={{ marginBottom: '-4px' }}
|
||||
type="default"
|
||||
/>
|
||||
);
|
||||
|
||||
case 'success':
|
||||
return (
|
||||
<Icon
|
||||
className="text-xl"
|
||||
component={IconRemove}
|
||||
data-testid="remove-tier"
|
||||
onClick={() => updateTier?.()}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<Button
|
||||
data-testid="select-tier-button"
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => handleTierSave(tier)}>
|
||||
{t('label.select')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getCardIcon = useCallback(
|
||||
(cardId: string) => {
|
||||
const isSelected = currentTier === cardId;
|
||||
const isActive = activeTier === cardId;
|
||||
|
||||
if ((isSelected && isActive) || isSelected) {
|
||||
return (
|
||||
<Icon
|
||||
className="text-xl"
|
||||
component={IconRemove}
|
||||
data-testid="remove-tier"
|
||||
onClick={() => updateTier?.()}
|
||||
/>
|
||||
);
|
||||
} else if (isActive) {
|
||||
return getTierSelectButton(cardId);
|
||||
if (data) {
|
||||
const tierData: CardWithListItems[] =
|
||||
data.map((tier: { name: string; description: string }) => ({
|
||||
id: `Tier${FQN_SEPARATOR_CHAR}${tier.name}`,
|
||||
title: tier.name,
|
||||
description: tier.description.substring(
|
||||
0,
|
||||
tier.description.indexOf('\n\n')
|
||||
),
|
||||
data: tier.description.substring(
|
||||
tier.description.indexOf('\n\n') + 1
|
||||
),
|
||||
})) ?? [];
|
||||
setTierData(tierData);
|
||||
} else {
|
||||
return (
|
||||
<Button
|
||||
ghost
|
||||
data-testid="select-tier-button"
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => handleTierSave(cardId)}>
|
||||
{t('label.select')}
|
||||
</Button>
|
||||
);
|
||||
setTierData([]);
|
||||
}
|
||||
},
|
||||
[currentTier, activeTier]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTier(currentTier);
|
||||
if (statusTier === 'waiting') {
|
||||
setStatusTier('success');
|
||||
setTimeout(() => {
|
||||
setInitialTierLoadingState();
|
||||
}, 300);
|
||||
} catch (err) {
|
||||
showErrorToast(
|
||||
err,
|
||||
t('server.entity-fetch-error', {
|
||||
entity: t('label.tier-plural-lowercase'),
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setIsLoadingTierData(false);
|
||||
}
|
||||
}, [currentTier]);
|
||||
};
|
||||
|
||||
const handleTierSelection = ({ target: { value } }: RadioChangeEvent) => {
|
||||
updateTier?.(value as string);
|
||||
};
|
||||
|
||||
const clearTierSelection = () => {
|
||||
updateTier?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
className="p-0 tier-card-popover"
|
||||
content={
|
||||
<Card
|
||||
className="tier-card"
|
||||
data-testid="cards"
|
||||
headStyle={{
|
||||
borderBottom: 'none',
|
||||
paddingLeft: '16px',
|
||||
paddingTop: '12px',
|
||||
}}
|
||||
title={
|
||||
<Row>
|
||||
<Col span={21}>
|
||||
<Typography.Title className="m-b-0" level={5}>
|
||||
{t('label.edit-entity', { entity: t('label.tier') })}
|
||||
</Typography.Title>
|
||||
</Col>
|
||||
</Row>
|
||||
<Space className="w-full p-xs justify-between">
|
||||
<Typography.Text className="m-b-0 font-medium text-md">
|
||||
{t('label.edit-entity', { entity: t('label.tier') })}
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
className="m-b-0 font-normal text-primary cursor-pointer"
|
||||
data-testid="clear-tier"
|
||||
onClick={clearTierSelection}>
|
||||
{t('label.clear')}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
}>
|
||||
<Collapse
|
||||
accordion
|
||||
className="collapse-container"
|
||||
defaultActiveKey={currentTier}
|
||||
onChange={(key) => handleCardSelection(key as string)}>
|
||||
{tierData.map((card) => (
|
||||
<Panel
|
||||
className={classNames('collapse-tier-panel', {
|
||||
selected: currentTier === card.id,
|
||||
})}
|
||||
data-testid="card-list"
|
||||
extra={<div data-testid="icon">{getCardIcon(card.id)}</div>}
|
||||
header={
|
||||
<Space direction="vertical" size={0}>
|
||||
<Typography.Paragraph className="m-b-0 text-color-inherit text-base font-semibold">
|
||||
{card.title}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph className="m-b-0 text-color-inherit font-medium">
|
||||
{card.description.replace(/\*/g, '')}
|
||||
</Typography.Paragraph>
|
||||
</Space>
|
||||
}
|
||||
key={card.id}>
|
||||
<RichTextEditorPreviewer
|
||||
enableSeeMoreVariant={false}
|
||||
markdown={card.data}
|
||||
/>
|
||||
</Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
<Radio.Group value={currentTier} onChange={handleTierSelection}>
|
||||
<Collapse
|
||||
accordion
|
||||
className="bg-white border-none"
|
||||
defaultActiveKey={currentTier}
|
||||
expandIconPosition="end">
|
||||
{tierData.map((card) => (
|
||||
<Panel
|
||||
data-testid="card-list"
|
||||
header={
|
||||
<div className="flex self-start">
|
||||
<Radio
|
||||
className="radio-input"
|
||||
data-testid="radio-btn"
|
||||
value={card.id}
|
||||
/>
|
||||
<Space direction="vertical" size={0}>
|
||||
<Typography.Paragraph className="m-b-0 font-regular text-grey-body">
|
||||
{card.title}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-muted">
|
||||
{card.description.replace(/\*/g, '')}
|
||||
</Typography.Paragraph>
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
key={card.id}>
|
||||
<RichTextEditorPreviewer
|
||||
className="tier-card-description"
|
||||
enableSeeMoreVariant={false}
|
||||
markdown={card.data}
|
||||
/>
|
||||
</Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
</Radio.Group>
|
||||
{isLoadingTierData && <Loader />}
|
||||
</Card>
|
||||
}
|
||||
data-testid="tier-card-container"
|
||||
overlayClassName="tier-card-container"
|
||||
placement="bottomRight"
|
||||
showArrow={false}
|
||||
trigger="click"
|
||||
|
@ -13,51 +13,61 @@
|
||||
|
||||
@import url('../../../styles/variables.less');
|
||||
|
||||
.tier-card {
|
||||
width: 760px;
|
||||
border: 1px solid #dde3ea;
|
||||
box-shadow: @box-shadow-base;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.tier-card-container {
|
||||
padding: 0;
|
||||
.ant-popover-inner {
|
||||
box-shadow: none;
|
||||
.ant-popover-inner-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-container {
|
||||
.collapse-tier-panel {
|
||||
border: @global-border;
|
||||
|
||||
.ant-collapse-content {
|
||||
border-color: @primary-color;
|
||||
}
|
||||
|
||||
&.ant-collapse-item-active {
|
||||
border: 1px solid @primary-color;
|
||||
background: @primary-color-hover;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border: 1px solid @primary-color;
|
||||
background: @primary-color;
|
||||
.ant-collapse-header {
|
||||
color: @white;
|
||||
.tier-card {
|
||||
width: 655px;
|
||||
.ant-card-body {
|
||||
.ant-collapse-item {
|
||||
.ant-collapse-header {
|
||||
.ant-collapse-arrow {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
}
|
||||
}
|
||||
.ant-collapse-content {
|
||||
&.ant-collapse-content-active {
|
||||
border: none;
|
||||
}
|
||||
.ant-collapse-content-box {
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&:first-child,
|
||||
&:first-child.ant-collapse-item-active {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
&:last-child {
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.tier-card-description {
|
||||
.toastui-editor-contents ul {
|
||||
padding-left: 18px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: @text-color-secondary;
|
||||
margin: 4px;
|
||||
li {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-collapse {
|
||||
background-color: @white;
|
||||
border: none;
|
||||
.radio-input .ant-radio {
|
||||
.ant-radio-inner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 1.5px solid @primary-color;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&:hover .ant-radio-inner {
|
||||
background-color: @primary-color-hover;
|
||||
}
|
||||
|
||||
&.ant-radio-checked .ant-radio-inner::after {
|
||||
box-shadow: none !important;
|
||||
transform: scale(0.35);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -118,6 +118,7 @@
|
||||
"classification-lowercase-plural": "classifications",
|
||||
"classification-plural": "Classifications",
|
||||
"clean-up-policy-plural-lowercase": "clean-up policies",
|
||||
"clear": "Clear",
|
||||
"clear-entity": "Clear {{entity}}",
|
||||
"click-here": "Click here",
|
||||
"client-email": "Client Email",
|
||||
|
@ -118,6 +118,7 @@
|
||||
"classification-lowercase-plural": "classifications",
|
||||
"classification-plural": "Clasificaciones",
|
||||
"clean-up-policy-plural-lowercase": "políticas de limpieza",
|
||||
"clear": "Clear",
|
||||
"clear-entity": "Limpiar {{entity}}",
|
||||
"click-here": "Haga clic aquí",
|
||||
"client-email": "Correo electrónico del cliente",
|
||||
|
@ -118,6 +118,7 @@
|
||||
"classification-lowercase-plural": "classifications",
|
||||
"classification-plural": "Classifications",
|
||||
"clean-up-policy-plural-lowercase": "Nettoyer les stratégies",
|
||||
"clear": "Clear",
|
||||
"clear-entity": "Effacer {{entity}}",
|
||||
"click-here": "Cliquer ici",
|
||||
"client-email": "Client Email",
|
||||
|
@ -118,6 +118,7 @@
|
||||
"classification-lowercase-plural": "classifications",
|
||||
"classification-plural": "分類",
|
||||
"clean-up-policy-plural-lowercase": "ポリシーの削除",
|
||||
"clear": "Clear",
|
||||
"clear-entity": "{{entity}}を削除",
|
||||
"click-here": "ここをクリック",
|
||||
"client-email": "Client Email",
|
||||
|
@ -118,6 +118,7 @@
|
||||
"classification-lowercase-plural": "classifications",
|
||||
"classification-plural": "Classificações",
|
||||
"clean-up-policy-plural-lowercase": "limpar políticas",
|
||||
"clear": "Clear",
|
||||
"clear-entity": "Limpar {{entity}}",
|
||||
"click-here": "Clique aqui",
|
||||
"client-email": "E-mail do cliente",
|
||||
|
@ -118,6 +118,7 @@
|
||||
"classification-lowercase-plural": "classifications",
|
||||
"classification-plural": "分类",
|
||||
"clean-up-policy-plural-lowercase": "清理策略",
|
||||
"clear": "Clear",
|
||||
"clear-entity": "清除{{entity}}",
|
||||
"click-here": "点击这里",
|
||||
"client-email": "客户端电子邮箱",
|
||||
|
Loading…
x
Reference in New Issue
Block a user