Fix UI : #4124 Remove Save and Discard button from manage tab (#4273)

This commit is contained in:
Sachin Chaurasiya 2022-04-20 23:36:56 +05:30 committed by GitHub
parent a977aa90bc
commit 8942c2b6bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 345 additions and 198 deletions

View File

@ -16,6 +16,8 @@ import {
faCheck,
faCheckCircle,
faCheckSquare,
faChevronDown,
faChevronRight,
faPlus,
faSearch,
faTimes,
@ -28,7 +30,16 @@ import { AuthProvider } from './authentication/auth-provider/AuthProvider';
import AppRouter from './router/AppRouter';
const App: FunctionComponent = () => {
library.add(faTimes, faCheck, faSearch, faPlus, faCheckSquare, faCheckCircle);
library.add(
faTimes,
faCheck,
faSearch,
faPlus,
faCheckSquare,
faCheckCircle,
faChevronDown,
faChevronRight
);
return (
<div className="main-container">

View File

@ -626,30 +626,6 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
onUpdate={onColumnsUpdate}
/>
</div>
{threadLink ? (
<ActivityThreadPanel
createThread={createThread}
deletePostHandler={deletePostHandler}
open={Boolean(threadLink)}
postFeedHandler={postFeedHandler}
threadLink={threadLink}
onCancel={onThreadPanelClose}
/>
) : null}
{selectedField ? (
<RequestDescriptionModal
createThread={createThread}
defaultValue={getDefaultValue(owner)}
header="Request description"
threadLink={getEntityFeedLink(
EntityType.TABLE,
datasetFQN,
selectedField
)}
onCancel={closeRequestModal}
/>
) : null}
</div>
)}
{activeTab === 2 && (
@ -776,6 +752,29 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
{getLoader()}
</div>
</div>
{threadLink ? (
<ActivityThreadPanel
createThread={createThread}
deletePostHandler={deletePostHandler}
open={Boolean(threadLink)}
postFeedHandler={postFeedHandler}
threadLink={threadLink}
onCancel={onThreadPanelClose}
/>
) : null}
{selectedField ? (
<RequestDescriptionModal
createThread={createThread}
defaultValue={getDefaultValue(owner)}
header="Request description"
threadLink={getEntityFeedLink(
EntityType.TABLE,
datasetFQN,
selectedField
)}
onCancel={closeRequestModal}
/>
) : null}
</div>
</div>
</PageContainer>

View File

@ -651,6 +651,7 @@ const EntityTable = ({
trigger="mouseenter">
<SVGIcons
alt="request-description"
className="tw-mt-2.5"
icon={Icons.REQUEST}
/>
</PopOver>

View File

@ -12,19 +12,21 @@
*/
import classNames from 'classnames';
import React, { FunctionComponent } from 'react';
import React, { CSSProperties, FunctionComponent } from 'react';
import './Loader.css';
type Props = {
size?: 'default' | 'small';
type?: 'default' | 'success' | 'error' | 'white';
className?: string;
style?: CSSProperties;
};
const Loader: FunctionComponent<Props> = ({
size = 'default',
type = 'default',
className = '',
style,
}: Props): JSX.Element => {
let classes = 'loader';
switch (size) {
@ -54,7 +56,11 @@ const Loader: FunctionComponent<Props> = ({
}
return (
<div className={classNames(classes, className)} data-testid="loader" />
<div
className={classNames(classes, className)}
data-testid="loader"
style={style}
/>
);
};

View File

@ -24,6 +24,7 @@ import { useAuthContext } from '../../authentication/auth-provider/AuthProvider'
import { deleteEntity } from '../../axiosAPIs/miscAPI';
import { getCategory } from '../../axiosAPIs/tagAPI';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { TITLE_FOR_NON_ADMIN_ACTION } from '../../constants/constants';
import { ENTITY_DELETE_STATE } from '../../constants/entity.constants';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import { useAuth } from '../../hooks/authHooks';
@ -38,7 +39,7 @@ import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import DropDownList from '../dropdown/DropDownList';
import Loader from '../Loader/Loader';
import EntityDeleteModal from '../Modals/EntityDeleteModal/EntityDeleteModal';
import { ManageProps } from './ManageTab.interface';
import { ManageProps, Status } from './ManageTab.interface';
const ManageTab: FunctionComponent<ManageProps> = ({
currentTier = '',
@ -57,10 +58,8 @@ const ManageTab: FunctionComponent<ManageProps> = ({
const { userPermissions, isAdminUser } = useAuth();
const { isAuthDisabled } = useAuthContext();
const [loading, setLoading] = useState<boolean>(false);
const [status, setStatus] = useState<'initial' | 'waiting' | 'success'>(
'initial'
);
const [statusOwner, setStatusOwner] = useState<Status>('initial');
const [statusTier, setStatusTier] = useState<Status>('initial');
const [activeTier, setActiveTier] = useState(currentTier);
const [listVisible, setListVisible] = useState(false);
const [teamJoinable, setTeamJoinable] = useState(isJoinable);
@ -80,6 +79,67 @@ const ManageTab: FunctionComponent<ManageProps> = ({
return allowTeamOwner ? ['Teams', 'Users'] : ['Users'];
};
const setInitialOwnerLoadingState = () => {
setStatusOwner('initial');
};
const setInitialTierLoadingState = () => {
setStatusTier('initial');
};
const prepareOwner = (updatedOwner: string) => {
return updatedOwner !== currentUser
? {
id: updatedOwner,
type: listOwners.find((item) => item.value === updatedOwner)?.type as
| 'user'
| 'team',
}
: undefined;
};
const prepareTier = (updatedTier: string) => {
return updatedTier !== currentTier ? updatedTier : undefined;
};
const handleOwnerSave = (updatedOwner: string, updatedTier: string) => {
setStatusOwner('waiting');
const newOwner: TableDetail['owner'] = prepareOwner(updatedOwner);
if (hideTier) {
if (newOwner || !isUndefined(teamJoinable)) {
onSave(newOwner, '', Boolean(teamJoinable)).catch(() => {
setInitialOwnerLoadingState();
});
} else {
setInitialOwnerLoadingState();
}
} else {
const newTier = prepareTier(updatedTier);
onSave(newOwner, newTier, Boolean(teamJoinable)).catch(() => {
setInitialOwnerLoadingState();
});
}
};
const handleTierSave = (updatedTier: string) => {
setStatusTier('waiting');
const newOwner: TableDetail['owner'] = prepareOwner(currentUser);
if (hideTier) {
if (newOwner || !isUndefined(teamJoinable)) {
onSave(newOwner, '', Boolean(teamJoinable)).catch(() => {
setInitialTierLoadingState();
});
} else {
setInitialTierLoadingState();
}
} else {
const newTier = prepareTier(updatedTier);
onSave(newOwner, newTier, Boolean(teamJoinable)).catch(() => {
setInitialTierLoadingState();
});
}
};
const handleOwnerSelection = (
_e: React.MouseEvent<HTMLElement, MouseEvent>,
value?: string
@ -88,6 +148,7 @@ const ManageTab: FunctionComponent<ManageProps> = ({
const newOwner = listOwners.find((item) => item.value === value);
if (newOwner) {
setOwner(newOwner.value);
handleOwnerSave(newOwner.value, activeTier);
}
}
setListVisible(false);
@ -97,45 +158,6 @@ const ManageTab: FunctionComponent<ManageProps> = ({
setActiveTier(cardId);
};
const setInitialLoadingState = () => {
setStatus('initial');
setLoading(false);
};
const handleSave = () => {
setLoading(true);
setStatus('waiting');
// Save API call goes here...
const newOwner: TableDetail['owner'] =
owner !== currentUser
? {
id: owner,
type: listOwners.find((item) => item.value === owner)?.type as
| 'user'
| 'team',
}
: undefined;
if (hideTier) {
if (newOwner || !isUndefined(teamJoinable)) {
onSave(newOwner, '', Boolean(teamJoinable)).catch(() => {
setInitialLoadingState();
});
} else {
setInitialLoadingState();
}
} else {
const newTier = activeTier !== currentTier ? activeTier : undefined;
onSave(newOwner, newTier, Boolean(teamJoinable)).catch(() => {
setInitialLoadingState();
});
}
};
const handleCancel = () => {
setActiveTier(currentTier);
setOwner(currentUser);
};
const handleOnEntityDelete = () => {
setEntityDeleteState((prev) => ({ ...prev, state: true }));
};
@ -193,7 +215,7 @@ const ManageTab: FunctionComponent<ManageProps> = ({
const getDeleteEntityWidget = () => {
return allowDelete && entityId && entityName && entityType ? (
<div className="tw-mt-9" data-testid="danger-zone">
<div className="tw-mt-1" data-testid="danger-zone">
<hr className="tw-border-main tw-mb-4" />
<div className="tw-border tw-border-error tw-px-4 tw-py-2 tw-flex tw-justify-between tw-rounded tw-mt-3 tw-shadow">
<div data-testid="danger-zone-text">
@ -204,16 +226,27 @@ const ManageTab: FunctionComponent<ManageProps> = ({
{`Once you delete this ${entityType}, it would be removed permanently`}
</p>
</div>
<Button
className="tw-px-2 tw-py-1 tw-rounded tw-h-auto tw-self-center tw-shadow"
data-testid="delete-button"
size="custom"
theme="primary"
type="button"
variant="outlined"
onClick={handleOnEntityDelete}>
Delete this {entityType}
</Button>
<NonAdminAction
className="tw-self-center"
html={
<Fragment>
<p>{TITLE_FOR_NON_ADMIN_ACTION}</p>
</Fragment>
}
isOwner={isAdminUser}
position="left">
<Button
className="tw-px-2 tw-py-1 tw-rounded tw-h-auto tw-self-center tw-shadow"
data-testid="delete-button"
disabled={!isAdminUser && !isAuthDisabled}
size="custom"
theme="primary"
type="button"
variant="outlined"
onClick={handleOnEntityDelete}>
Delete this {entityType}
</Button>
</NonAdminAction>
</div>
</div>
) : null;
@ -240,6 +273,9 @@ const ManageTab: FunctionComponent<ManageProps> = ({
<CardListItem
card={card}
isActive={activeTier === card.id}
isSelected={card.id === currentTier}
tierStatus={statusTier}
onSave={handleTierSave}
onSelect={handleCardSelection}
/>
</NonAdminAction>
@ -297,10 +333,8 @@ const ManageTab: FunctionComponent<ManageProps> = ({
);
setTierData(tierData);
setIsLoadingTierData(false);
} else {
setTierData([]);
setIsLoadingTierData(false);
}
})
.catch((err: AxiosError) => {
@ -308,9 +342,29 @@ const ManageTab: FunctionComponent<ManageProps> = ({
err,
jsonData['api-error-messages']['fetch-tiers-error']
);
})
.finally(() => {
setIsLoadingTierData(false);
});
};
const getOwnerUpdateLoader = () => {
return (
<span className="tw-ml-4">
{statusOwner === 'waiting' ? (
<Loader
className="tw-inline-block"
size="small"
style={{ marginBottom: '-4px' }}
type="default"
/>
) : statusOwner === 'success' ? (
<FontAwesomeIcon icon="check" />
) : null}
</span>
);
};
useEffect(() => {
if (!hideTier) {
getTierData();
@ -326,16 +380,22 @@ const ManageTab: FunctionComponent<ManageProps> = ({
}, [currentUser]);
useEffect(() => {
setTimeout(() => {
setLoading(false);
}, 1000);
if (status === 'waiting') {
setStatus('success');
if (statusOwner === 'waiting') {
setStatusOwner('success');
setTimeout(() => {
setStatus('initial');
setInitialOwnerLoadingState();
}, 3000);
}
}, [currentTier, currentUser]);
}, [currentUser]);
useEffect(() => {
if (statusTier === 'waiting') {
setStatusTier('success');
setTimeout(() => {
setInitialTierLoadingState();
}, 3000);
}
}, [currentTier]);
useEffect(() => {
setListOwners(getOwnerList());
@ -350,7 +410,10 @@ const ManageTab: FunctionComponent<ManageProps> = ({
className="tw-max-w-3xl tw-mx-auto"
data-testid="manage-tab"
id="manageTabDetails">
<div className="tw-mt-2 tw-mb-4 tw-pb-4 tw-border-b tw-border-separator ">
<div
className={classNames('tw-mt-2 tw-pb-4', {
'tw-border-b tw-border-separator tw-mb-4': !hideTier,
})}>
<div>
<span className="tw-mr-2">Owner:</span>
<span className="tw-relative">
@ -410,49 +473,12 @@ const ManageTab: FunctionComponent<ManageProps> = ({
onSelect={handleOwnerSelection}
/>
)}
{getOwnerUpdateLoader()}
</span>
</div>
{getJoinableWidget()}
</div>
{getTierCards()}
<div className="tw-mt-6 tw-text-right" data-testid="buttons">
<Button
size="regular"
theme="primary"
variant="text"
onClick={handleCancel}>
Discard
</Button>
{loading ? (
<Button
disabled
className="tw-w-16 tw-h-10 disabled:tw-opacity-100"
size="regular"
theme="primary"
variant="contained">
<Loader size="small" type="white" />
</Button>
) : status === 'success' ? (
<Button
disabled
className="tw-w-16 tw-h-10 disabled:tw-opacity-100"
size="regular"
theme="primary"
variant="contained">
<FontAwesomeIcon icon="check" />
</Button>
) : (
<Button
className="tw-w-16 tw-h-10"
data-testid="saveManageTab"
size="regular"
theme="primary"
variant="contained"
onClick={handleSave}>
Save
</Button>
)}
</div>
{getDeleteEntityWidget()}
{getDeleteModal()}
</div>

View File

@ -30,3 +30,5 @@ export interface ManageProps {
entityType?: string;
allowDelete?: boolean;
}
export type Status = 'initial' | 'waiting' | 'success';

View File

@ -14,7 +14,6 @@
import {
findAllByTestId,
findByTestId,
findByText,
fireEvent,
render,
} from '@testing-library/react';
@ -92,42 +91,6 @@ describe('Test Manage tab Component', () => {
expect(card.length).toBe(3);
});
it('there should be 2 buttons', async () => {
const { container } = render(
<ManageTab hasEditAccess onSave={mockFunction} />
);
const buttons = await findByTestId(container, 'buttons');
expect(buttons.childElementCount).toBe(2);
});
it('Onclick of save, onSave function also called', async () => {
const { container } = render(
<ManageTab hasEditAccess onSave={mockFunction} />
);
const card = await findAllByTestId(container, 'card');
fireEvent.click(
card[1],
new MouseEvent('click', {
bubbles: true,
cancelable: true,
})
);
const save = await findByText(container, /Save/i);
fireEvent.click(
save,
new MouseEvent('click', {
bubbles: true,
cancelable: true,
})
);
expect(mockFunction).toBeCalledTimes(1);
});
it('Should render switch if isJoinable is present', async () => {
const { container } = render(
<ManageTab hasEditAccess isJoinable onSave={mockFunction} />

View File

@ -23,7 +23,7 @@ import AddRoleModal from './AddRoleModal';
const mockCancel = jest.fn();
const mockSave = jest.fn();
const mockForm = jest.fn().mockReturnValue(<p data-testid="form">data</p>);
const mockInitionalData = {
const mockInitialData = {
name: '',
description: '',
};
@ -34,7 +34,7 @@ describe('Test AddRoleModal component', () => {
<AddRoleModal
form={mockForm}
header="Adding new users"
initialData={mockInitionalData}
initialData={mockInitialData}
onCancel={mockCancel}
onSave={mockSave}
/>
@ -55,7 +55,7 @@ describe('Test AddRoleModal component', () => {
<AddRoleModal
form={mockForm}
header="Adding new users"
initialData={mockInitionalData}
initialData={mockInitialData}
onCancel={mockCancel}
onSave={mockSave}
/>
@ -71,7 +71,7 @@ describe('Test AddRoleModal component', () => {
<AddRoleModal
form={mockForm}
header="Adding new users"
initialData={mockInitionalData}
initialData={mockInitialData}
onCancel={mockCancel}
onSave={mockSave}
/>

View File

@ -516,6 +516,7 @@ const PipelineDetails = ({
trigger="mouseenter">
<SVGIcons
alt="request-description"
className="tw-mt-2.5"
icon={Icons.REQUEST}
/>
</PopOver>

View File

@ -11,6 +11,8 @@
* limitations under the License.
*/
import { Status } from '../../ManageTab/ManageTab.interface';
export type CardWithListItems = {
id: string;
description: string;
@ -21,5 +23,8 @@ export type CardWithListItems = {
export type Props = {
card: CardWithListItems;
isActive: boolean;
isSelected: boolean;
tierStatus: Status;
onSave: (updatedTier: string) => void;
onSelect: (cardId: string) => void;
};

View File

@ -14,11 +14,13 @@
export const cardStyle = {
base: 'tw-flex tw-flex-col tw-rounded-md tw-border tw-mb-4',
default: 'tw-border-main',
active: 'tw-border-primary',
active: 'tw-border-primary-lite',
selected: 'tw-border-primary',
header: {
base: 'tw-flex tw-px-5 tw-py-3 tw-cursor-pointer tw-justify-between tw-items-center',
default: 'tw-bg-badge',
active: 'tw-bg-primary tw-rounded-t-md tw-text-white',
active: 'tw-bg-primary-lite tw-rounded-t-md',
selected: 'tw-bg-primary tw-rounded-t-md tw-text-white',
title: 'tw-text-base tw-mb-0',
description: 'tw-font-medium tw-pr-2',
},
@ -26,6 +28,7 @@ export const cardStyle = {
base: 'tw-py-5 tw-px-10',
default: 'tw-hidden',
active: 'tw-block',
selected: 'tw-block',
content: {
withBorder: 'tw-py-3 tw-border-b tw-border-main',
withoutBorder: 'tw-py-1',

View File

@ -15,7 +15,8 @@ import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import CardListItem from './CardWithListItems';
const mockFunction = jest.fn();
const mockSelectFunction = jest.fn();
const mockSaveFuntion = jest.fn();
const mockCard = {
id: 'test1',
title: 'card',
@ -30,7 +31,14 @@ jest.mock('../../common/rich-text-editor/RichTextEditorPreviewer', () => {
describe('Test CardWithListing Component', () => {
it('Component should render', () => {
const { getByTestId } = render(
<CardListItem card={mockCard} isActive={false} onSelect={mockFunction} />
<CardListItem
card={mockCard}
isActive={false}
isSelected={false}
tierStatus="initial"
onSave={mockSaveFuntion}
onSelect={mockSelectFunction}
/>
);
const card = getByTestId('card-list');
@ -40,9 +48,16 @@ describe('Test CardWithListing Component', () => {
expect(getByTestId('icon')).toBeEmptyDOMElement();
});
it('OnClick callback function should call', () => {
it('OnClick onSelect function should call', () => {
const { getByTestId } = render(
<CardListItem card={mockCard} isActive={false} onSelect={mockFunction} />
<CardListItem
card={mockCard}
isActive={false}
isSelected={false}
tierStatus="initial"
onSave={mockSaveFuntion}
onSelect={mockSelectFunction}
/>
);
const card = getByTestId('card-list');
@ -54,6 +69,66 @@ describe('Test CardWithListing Component', () => {
})
);
expect(mockFunction).toHaveBeenCalledTimes(1);
expect(mockSelectFunction).toHaveBeenCalledTimes(1);
});
it('OnClick onSelect function should call and Select tier button should be visible', () => {
const { getByTestId } = render(
<CardListItem
isActive
card={mockCard}
isSelected={false}
tierStatus="initial"
onSave={mockSaveFuntion}
onSelect={mockSelectFunction}
/>
);
const card = getByTestId('card-list');
fireEvent(
card,
new MouseEvent('click', {
bubbles: true,
cancelable: true,
})
);
expect(mockSelectFunction).toHaveBeenCalledTimes(1);
const tierSelectButton = getByTestId('select-tier-buuton');
expect(tierSelectButton).toBeInTheDocument();
});
it('onClick of select tier button onSave function should call.', () => {
const { getByTestId } = render(
<CardListItem
isActive
card={mockCard}
isSelected={false}
tierStatus="initial"
onSave={mockSaveFuntion}
onSelect={mockSelectFunction}
/>
);
const card = getByTestId('card-list');
fireEvent(
card,
new MouseEvent('click', {
bubbles: true,
cancelable: true,
})
);
expect(mockSelectFunction).toHaveBeenCalledTimes(1);
const tierSelectButton = getByTestId('select-tier-buuton');
expect(tierSelectButton).toBeInTheDocument();
fireEvent.click(tierSelectButton);
expect(mockSaveFuntion).toHaveBeenCalledTimes(1);
});
});

View File

@ -14,39 +14,90 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import React, { FunctionComponent } from 'react';
import { Button } from '../../buttons/Button/Button';
import RichTextEditorPreviewer from '../../common/rich-text-editor/RichTextEditorPreviewer';
import Loader from '../../Loader/Loader';
import { Props } from './CardWithListItems.interface';
import { cardStyle } from './CardWithListItems.style';
const CardListItem: FunctionComponent<Props> = ({
card,
isActive,
isSelected,
onSelect,
onSave,
tierStatus,
}: Props) => {
const getCardBodyStyle = () => {
return isSelected
? cardStyle.selected
: isActive
? cardStyle.active
: cardStyle.default;
};
const getCardHeaderStyle = () => {
return isSelected
? cardStyle.header.selected
: isActive
? cardStyle.header.active
: cardStyle.header.default;
};
const getTierSelectButton = (tier: string) => {
return tierStatus === 'waiting' ? (
<Loader
className="tw-inline-block"
size="small"
style={{ marginBottom: '-4px' }}
type="default"
/>
) : tierStatus === 'success' ? (
<FontAwesomeIcon icon="check" />
) : (
<Button
data-testid="select-tier-buuton"
size="small"
theme="primary"
onClick={() => onSave(tier)}>
Select
</Button>
);
};
const getCardIcon = (cardId: string) => {
if (isSelected && isActive) {
return <FontAwesomeIcon className="tw-text-h4" icon="check-circle" />;
} else if (isSelected) {
return <FontAwesomeIcon className="tw-text-h4" icon="check-circle" />;
} else if (isActive) {
return getTierSelectButton(cardId);
} else {
return null;
}
};
return (
<div
className={classNames(
cardStyle.base,
isActive ? cardStyle.active : cardStyle.default
)}
className={classNames(cardStyle.base, getCardBodyStyle())}
data-testid="card-list"
onClick={() => onSelect(card.id)}>
<div
className={classNames(
cardStyle.header.base,
isActive ? cardStyle.header.active : cardStyle.header.default
)}>
<div className="tw-flex tw-flex-col">
<h4 className={cardStyle.header.title}>{card.title}</h4>
<p className={cardStyle.header.description}>
{card.description.replace(/\*/g, '')}
</p>
</div>
<div data-testid="icon">
{isActive && (
<FontAwesomeIcon className="tw-text-h2" icon="check-circle" />
)}
<div className={classNames(cardStyle.header.base, getCardHeaderStyle())}>
<div className="tw-flex">
<div className="tw-self-start tw-mr-2">
<FontAwesomeIcon
className="tw-text-xs"
icon={isActive ? 'chevron-down' : 'chevron-right'}
/>
</div>
<div className="tw-flex tw-flex-col">
<h4 className={cardStyle.header.title}>{card.title}</h4>
<p className={cardStyle.header.description}>
{card.description.replace(/\*/g, '')}
</p>
</div>
</div>
<div data-testid="icon">{getCardIcon(card.id)}</div>
</div>
<div
className={classNames(

View File

@ -132,7 +132,11 @@ const Description = ({
position="top"
title="Request description"
trigger="mouseenter">
<SVGIcons alt="request-description" icon={Icons.REQUEST} />
<SVGIcons
alt="request-description"
className="tw-mt-2"
icon={Icons.REQUEST}
/>
</PopOver>
</button>
) : null}