ui : supported delete functionality for sample data (#12871)

This commit is contained in:
Ashish Gupta 2023-08-17 17:29:07 +05:30 committed by GitHub
parent 795294c87f
commit c6d03a6eaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 252 additions and 50 deletions

View File

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<path fill="#37352F" d="M20.184 2.813H16.67v-.704C16.669.946 15.722 0 14.559 0H8.934C7.771 0 6.825.946 6.825 2.11v.703H3.309c-1.163 0-2.109.946-2.109 2.109 0 .934.61 1.728 1.453 2.004l1.254 15.14A2.122 2.122 0 0 0 6.01 24h11.474c1.089 0 2.012-.85 2.102-1.935L20.84 6.926a2.113 2.113 0 0 0 1.454-2.004c0-1.163-.947-2.11-2.11-2.11ZM8.231 2.109c0-.387.316-.703.703-.703h5.625c.388 0 .704.316.704.703v.704H8.23v-.704Zm9.954 19.84a.707.707 0 0 1-.7.645H6.01a.707.707 0 0 1-.701-.645L4.073 7.031h15.348L18.184 21.95Zm2-16.324H3.308a.704.704 0 0 1 0-1.406h16.875a.704.704 0 0 1 0 1.406Z"/><path fill="#37352F" d="M9.186 20.44 8.483 9.098a.704.704 0 0 0-1.404.087l.704 11.344a.703.703 0 1 0 1.403-.087ZM12 8.438a.703.703 0 0 0-.703.703v11.343a.703.703 0 0 0 1.406 0V9.141A.703.703 0 0 0 12 8.437ZM16.262 8.439a.704.704 0 0 0-.745.658l-.703 11.344a.703.703 0 0 0 1.403.087l.704-11.344a.703.703 0 0 0-.659-.745Z"/>
<path fill="currentColor" d="M20.184 2.813H16.67v-.704C16.669.946 15.722 0 14.559 0H8.934C7.771 0 6.825.946 6.825 2.11v.703H3.309c-1.163 0-2.109.946-2.109 2.109 0 .934.61 1.728 1.453 2.004l1.254 15.14A2.122 2.122 0 0 0 6.01 24h11.474c1.089 0 2.012-.85 2.102-1.935L20.84 6.926a2.113 2.113 0 0 0 1.454-2.004c0-1.163-.947-2.11-2.11-2.11ZM8.231 2.109c0-.387.316-.703.703-.703h5.625c.388 0 .704.316.704.703v.704H8.23v-.704Zm9.954 19.84a.707.707 0 0 1-.7.645H6.01a.707.707 0 0 1-.701-.645L4.073 7.031h15.348L18.184 21.95Zm2-16.324H3.308a.704.704 0 0 1 0-1.406h16.875a.704.704 0 0 1 0 1.406Z"/><path fill="currentColor" d="M9.186 20.44 8.483 9.098a.704.704 0 0 0-1.404.087l.704 11.344a.703.703 0 1 0 1.403-.087ZM12 8.438a.703.703 0 0 0-.703.703v11.343a.703.703 0 0 0 1.406 0V9.141A.703.703 0 0 0 12 8.437ZM16.262 8.439a.704.704 0 0 0-.745.658l-.703 11.344a.703.703 0 0 0 1.403.087l.704-11.344a.703.703 0 0 0-.659-.745Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 984 B

After

Width:  |  Height:  |  Size: 994 B

View File

@ -11,19 +11,39 @@
* limitations under the License.
*/
import { Space, Table as AntdTable, Typography } from 'antd';
import {
Button,
Dropdown,
Space,
Table as AntdTable,
Tooltip,
Typography,
} from 'antd';
import { ItemType } from 'antd/lib/menu/hooks/useItems';
import AppState from 'AppState';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { ManageButtonItemLabel } from 'components/common/ManageButtonContentItem/ManageButtonContentItem.component';
import EntityDeleteModal from 'components/Modals/EntityDeleteModal/EntityDeleteModal';
import { useTourProvider } from 'components/TourProvider/TourProvider';
import { DROPDOWN_ICON_SIZE_PROPS } from 'constants/ManageButton.constants';
import { mockDatasetData } from 'constants/mockTourData.constants';
import { LOADING_STATE } from 'enums/common.enum';
import { EntityType } from 'enums/entity.enum';
import { t } from 'i18next';
import { isEmpty, lowerCase } from 'lodash';
import React, { useEffect, useState } from 'react';
import { getSampleDataByTableId } from 'rest/tableAPI';
import { observer } from 'mobx-react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
deleteSampleDataByTableId,
getSampleDataByTableId,
} from 'rest/tableAPI';
import { ReactComponent as IconDelete } from '../../assets/svg/ic-delete.svg';
import { ReactComponent as IconDropdown } from '../../assets/svg/menu.svg';
import { WORKFLOWS_PROFILER_DOCS } from '../../constants/docs.constants';
import { Table } from '../../generated/entity/data/table';
import { withLoader } from '../../hoc/withLoader';
import { Transi18next } from '../../utils/CommonUtils';
import { getEntityDeleteMessage, Transi18next } from '../../utils/CommonUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder';
import Loader from '../Loader/Loader';
@ -34,11 +54,38 @@ import {
SampleDataType,
} from './sample.interface';
import './SampleDataTable.style.less';
const SampleDataTable = ({ isTableDeleted, tableId }: SampleDataProps) => {
const SampleDataTable = ({
isTableDeleted,
tableId,
ownerId,
permissions,
}: SampleDataProps) => {
const { isTourPage } = useTourProvider();
const [sampleData, setSampleData] = useState<SampleData>();
const [isLoading, setIsLoading] = useState(true);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false);
const [deleteState, setDeleteState] = useState(LOADING_STATE.INITIAL);
const [showActions, setShowActions] = useState(false);
const currentUser = useMemo(
() => AppState.getCurrentUserDetails(),
[AppState.userDetails]
);
const hasPermission = useMemo(
() =>
permissions.EditAll ||
permissions.EditSampleData ||
currentUser?.id === ownerId,
[ownerId, permissions, currentUser]
);
const handleDeleteModal = useCallback(
() => setIsDeleteModalOpen((prev) => !prev),
[]
);
const getSampleDataWithType = (table: Table) => {
const { sampleData, columns } = table;
@ -91,6 +138,52 @@ const SampleDataTable = ({ isTableDeleted, tableId }: SampleDataProps) => {
}
};
const handleDeleteSampleData = async () => {
setDeleteState(LOADING_STATE.WAITING);
try {
await deleteSampleDataByTableId(tableId);
handleDeleteModal();
fetchSampleData();
} catch (error) {
showErrorToast(
error as AxiosError,
t('server.delete-entity-error', {
entity: t('label.sample-data'),
})
);
} finally {
setDeleteState(LOADING_STATE.SUCCESS);
}
};
const manageButtonContent: ItemType[] = [
{
label: (
<ManageButtonItemLabel
description={t('message.delete-entity-type-action-description', {
entityType: t('label.sample-data'),
})}
icon={
<IconDelete
className="m-t-xss"
{...DROPDOWN_ICON_SIZE_PROPS}
name="Delete"
/>
}
id="delete-button"
name={t('label.delete')}
/>
),
key: 'delete-button',
onClick: (e) => {
e.domEvent.stopPropagation();
setShowActions(false);
handleDeleteModal();
},
},
];
useEffect(() => {
setIsLoading(true);
if (!isTableDeleted && tableId && !isTourPage) {
@ -114,7 +207,7 @@ const SampleDataTable = ({ isTableDeleted, tableId }: SampleDataProps) => {
if (isEmpty(sampleData?.rows) && isEmpty(sampleData?.columns)) {
return (
<ErrorPlaceHolder>
<ErrorPlaceHolder className="error-placeholder">
<Typography.Paragraph>
<Transi18next
i18nKey="message.view-sample-data-entity"
@ -142,6 +235,30 @@ const SampleDataTable = ({ isTableDeleted, tableId }: SampleDataProps) => {
})}
data-testid="sample-data"
id="sampleDataDetails">
<Space className="m-b-md justify-end w-full">
{hasPermission && (
<Dropdown
menu={{
items: manageButtonContent,
}}
open={showActions}
overlayClassName="manage-dropdown-list-container"
overlayStyle={{ width: '350px' }}
placement="bottomRight"
trigger={['click']}
onOpenChange={setShowActions}>
<Tooltip placement="right">
<Button
className="flex-center px-1.5"
data-testid="sample-data-manage-button"
onClick={() => setShowActions(true)}>
<IconDropdown className="anticon self-center " />
</Button>
</Tooltip>
</Dropdown>
)}
</Space>
<AntdTable
bordered
columns={sampleData?.columns}
@ -152,8 +269,20 @@ const SampleDataTable = ({ isTableDeleted, tableId }: SampleDataProps) => {
scroll={{ x: true }}
size="small"
/>
{isDeleteModalOpen && (
<EntityDeleteModal
bodyText={getEntityDeleteMessage(t('label.sample-data'), '')}
entityName={t('label.sample-data')}
entityType={EntityType.SAMPLE_DATA}
loadingState={deleteState}
visible={isDeleteModalOpen}
onCancel={handleDeleteModal}
onConfirm={handleDeleteSampleData}
/>
)}
</div>
);
};
export default withLoader<SampleDataProps>(SampleDataTable);
export default withLoader<SampleDataProps>(observer(SampleDataTable));

View File

@ -11,11 +11,6 @@
* limitations under the License.
*/
@border-color: #e5e7eb;
.no-data-placeholder {
width: 100%;
padding: 32px;
border: 1px solid @border-color;
border-radius: 4px;
.error-placeholder {
height: calc(100vh - 255px);
}

View File

@ -12,11 +12,27 @@
*/
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface';
import React from 'react';
import { getSampleDataByTableId } from 'rest/tableAPI';
import { MOCK_TABLE } from '../../mocks/TableData.mock';
import SampleDataTable from './SampleDataTable.component';
const mockProps = {
tableId: 'id',
ownerId: 'ownerId',
permissions: {
Create: true,
Delete: true,
ViewAll: true,
EditAll: true,
EditDescription: true,
EditDisplayName: true,
EditCustomFields: true,
} as OperationPermission,
};
jest.mock('react-router-dom', () => ({
Link: jest.fn().mockImplementation(({ children }) => <span>{children}</span>),
useLocation: jest.fn().mockImplementation(() => ({ pathname: 'test' })),
@ -36,6 +52,10 @@ jest.mock('../common/error-with-placeholder/ErrorPlaceHolder', () => {
);
});
jest.mock('components/Modals/EntityDeleteModal/EntityDeleteModal', () => {
return jest.fn().mockReturnValue(<p>EntityDeleteModal</p>);
});
describe('Test SampleDataTable Component', () => {
it('Render error placeholder if the columns passed are empty', async () => {
(getSampleDataByTableId as jest.Mock).mockImplementationOnce(() =>
@ -43,7 +63,7 @@ describe('Test SampleDataTable Component', () => {
);
await act(async () => {
render(<SampleDataTable tableId="id" />);
render(<SampleDataTable {...mockProps} />);
});
const errorPlaceholder = screen.getByTestId('error-placeholder');
@ -53,11 +73,51 @@ describe('Test SampleDataTable Component', () => {
it('Renders all the data that was sent to the component', async () => {
await act(async () => {
render(<SampleDataTable tableId="id" />);
render(<SampleDataTable {...mockProps} />);
});
const deleteButton = screen.getByTestId('sample-data-manage-button');
const table = screen.getByTestId('sample-data-table');
expect(deleteButton).toBeInTheDocument();
expect(table).toBeInTheDocument();
});
it('Sample Data menu dropdown should not be present when not have permission', async () => {
await act(async () => {
render(
<SampleDataTable
{...mockProps}
permissions={{
...mockProps.permissions,
EditAll: false,
}}
/>
);
});
expect(
screen.queryByTestId('sample-data-manage-button')
).not.toBeInTheDocument();
});
it('Render Delete Modal when delete sample data button is clicked', async () => {
await act(async () => {
render(<SampleDataTable {...mockProps} />);
});
const dropdown = screen.getByTestId('sample-data-manage-button');
expect(dropdown).toBeInTheDocument();
userEvent.click(dropdown);
const deleteButton = screen.getByTestId('delete-button-details-container');
userEvent.click(deleteButton);
const deleteModal = screen.getByText('EntityDeleteModal');
expect(deleteModal).toBeInTheDocument();
});
});

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { ColumnsType } from 'antd/lib/table';
import { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface';
export type SampleDataType =
| string
@ -29,4 +30,6 @@ export interface SampleData {
export interface SampleDataProps {
isTableDeleted?: boolean;
tableId: string;
ownerId: string;
permissions: OperationPermission;
}

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { Popover } from 'antd';
import { Button, Popover, Space } from 'antd';
import { t } from 'i18next';
import { isEmpty } from 'lodash';
import React, {
@ -25,11 +25,12 @@ import { useHistory } from 'react-router-dom';
import { getUserByName } from 'rest/userAPI';
import { getEntityName } from 'utils/EntityUtils';
import AppState from '../../../AppState';
import { ReactComponent as IconTeams } from '../../../assets/svg/teams-grey.svg';
import { ReactComponent as IconUsers } from '../../../assets/svg/user.svg';
import { getUserPath, TERM_ADMIN } from '../../../constants/constants';
import { User } from '../../../generated/entity/teams/user';
import { EntityReference } from '../../../generated/type/entityReference';
import { getNonDeletedTeams } from '../../../utils/CommonUtils';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import Loader from '../../Loader/Loader';
import ProfilePicture from '../ProfilePicture/ProfilePicture';
@ -44,9 +45,9 @@ const UserPopOverCard: FC<Props> = ({ children, userName, type = 'user' }) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const getData = () => {
const userdetails = AppState.userDataProfiles[userName];
if (userdetails) {
setUserData(userdetails);
const userDetails = AppState.userDataProfiles[userName];
if (userDetails) {
setUserData(userDetails);
setIsLoading(false);
} else {
if (type === 'user') {
@ -68,21 +69,24 @@ const UserPopOverCard: FC<Props> = ({ children, userName, type = 'user' }) => {
const teams = getNonDeletedTeams(userData.teams ?? []);
return teams?.length ? (
<p className="m-t-xs">
<SVGIcons alt="icon" className="w-4" icon={Icons.TEAMS_GREY} />
<span className="m-r-xs m-l-xss align-middle font-medium">
{t('label.team-plural')}
</span>
<span className="d-flex flex-wrap m-t-xss">
{teams.map((team, i) => (
<div className="m-t-xs">
<p className="d-flex items-center">
<IconTeams height={16} width={16} />
<span className="m-r-xs m-l-xss align-middle font-medium">
{t('label.team-plural')}
</span>
</p>
<p className="d-flex flex-wrap m-t-xss">
{teams.map((team) => (
<span
className="bg-grey rounded-4 p-x-xs text-grey-body text-xs"
key={i}>
className="bg-grey rounded-4 p-x-xs text-grey-body text-xs m-b-xss"
key={team.id}>
{team?.displayName ?? team?.name}
</span>
))}
</span>
</p>
</p>
</div>
) : null;
};
@ -91,24 +95,29 @@ const UserPopOverCard: FC<Props> = ({ children, userName, type = 'user' }) => {
const isAdmin = userData?.isAdmin;
return roles?.length ? (
<p className="m-t-xs">
<SVGIcons alt="icon" className="w-4" icon={Icons.USERS} />
<span className="m-r-xs m-l-xss align-middle font-medium">
{t('label.role-plural')}
</span>
<div className="m-t-xs">
<p className="d-flex items-center">
<IconUsers height={16} width={16} />
<span className="m-r-xs m-l-xss align-middle font-medium">
{t('label.role-plural')}
</span>
</p>
<span className="d-flex flex-wrap m-t-xss">
{isAdmin && (
<span className="bg-grey rounded-4 p-x-xs text-xs">
<span className="bg-grey rounded-4 p-x-xs text-xs m-b-xss">
{TERM_ADMIN}
</span>
)}
{roles.map((role, i) => (
<span className="bg-grey rounded-4 p-x-xs text-xs" key={i}>
{roles.map((role) => (
<span
className="bg-grey rounded-4 p-x-xs text-xs m-b-xss"
key={role.id}>
{role?.displayName ?? role?.name}
</span>
))}
</span>
</p>
</div>
) : null;
};
@ -117,25 +126,24 @@ const UserPopOverCard: FC<Props> = ({ children, userName, type = 'user' }) => {
const displayName = getEntityName(userData as unknown as EntityReference);
return (
<div className="d-flex">
<div className="m-r-xs">
<ProfilePicture id="" name={userName} width="24" />
</div>
<Space align="center">
<ProfilePicture id="" name={userName} width="24" />
<div className="self-center">
<button
className="text-info"
<Button
className="text-info p-0"
type="link"
onClick={(e) => {
e.stopPropagation();
onTitleClickHandler(getUserPath(name));
}}>
<span className="font-medium m-r-xs">{displayName}</span>
</button>
</Button>
{displayName !== name ? (
<span className="text-grey-muted">{name}</span>
) : null}
{isEmpty(userData) && <span>{userName}</span>}
</div>
</div>
</Space>
);
};

View File

@ -47,6 +47,7 @@ export enum EntityType {
SUBSCRIPTION = 'subscription',
USER_NAME = 'username',
CHART = 'chart',
SAMPLE_DATA = 'sampleData',
}
export enum AssetsType {

View File

@ -563,6 +563,8 @@ const TableDetailsPageV1 = () => {
) : (
<SampleDataTableComponent
isTableDeleted={tableDetails?.deleted}
ownerId={tableDetails?.owner?.id ?? ''}
permissions={tablePermissions}
tableId={tableDetails?.id ?? ''}
/>
),

View File

@ -246,3 +246,7 @@ export const getTableList = async (params?: TableListParams) => {
return response.data;
};
export const deleteSampleDataByTableId = async (id: string) => {
return await APIClient.delete<Table>(`/tables/${id}/sampleData`);
};