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"> <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> </svg>

Before

Width:  |  Height:  |  Size: 984 B

After

Width:  |  Height:  |  Size: 994 B

View File

@ -11,19 +11,39 @@
* limitations under the License. * 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 { AxiosError } from 'axios';
import classNames from 'classnames'; 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 { useTourProvider } from 'components/TourProvider/TourProvider';
import { DROPDOWN_ICON_SIZE_PROPS } from 'constants/ManageButton.constants';
import { mockDatasetData } from 'constants/mockTourData.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 { t } from 'i18next';
import { isEmpty, lowerCase } from 'lodash'; import { isEmpty, lowerCase } from 'lodash';
import React, { useEffect, useState } from 'react'; import { observer } from 'mobx-react';
import { getSampleDataByTableId } from 'rest/tableAPI'; 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 { WORKFLOWS_PROFILER_DOCS } from '../../constants/docs.constants';
import { Table } from '../../generated/entity/data/table'; import { Table } from '../../generated/entity/data/table';
import { withLoader } from '../../hoc/withLoader'; import { withLoader } from '../../hoc/withLoader';
import { Transi18next } from '../../utils/CommonUtils'; import { getEntityDeleteMessage, Transi18next } from '../../utils/CommonUtils';
import { showErrorToast } from '../../utils/ToastUtils'; import { showErrorToast } from '../../utils/ToastUtils';
import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder'; import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder';
import Loader from '../Loader/Loader'; import Loader from '../Loader/Loader';
@ -34,11 +54,38 @@ import {
SampleDataType, SampleDataType,
} from './sample.interface'; } from './sample.interface';
import './SampleDataTable.style.less'; import './SampleDataTable.style.less';
const SampleDataTable = ({ isTableDeleted, tableId }: SampleDataProps) => {
const SampleDataTable = ({
isTableDeleted,
tableId,
ownerId,
permissions,
}: SampleDataProps) => {
const { isTourPage } = useTourProvider(); const { isTourPage } = useTourProvider();
const [sampleData, setSampleData] = useState<SampleData>(); const [sampleData, setSampleData] = useState<SampleData>();
const [isLoading, setIsLoading] = useState(true); 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 getSampleDataWithType = (table: Table) => {
const { sampleData, columns } = 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(() => { useEffect(() => {
setIsLoading(true); setIsLoading(true);
if (!isTableDeleted && tableId && !isTourPage) { if (!isTableDeleted && tableId && !isTourPage) {
@ -114,7 +207,7 @@ const SampleDataTable = ({ isTableDeleted, tableId }: SampleDataProps) => {
if (isEmpty(sampleData?.rows) && isEmpty(sampleData?.columns)) { if (isEmpty(sampleData?.rows) && isEmpty(sampleData?.columns)) {
return ( return (
<ErrorPlaceHolder> <ErrorPlaceHolder className="error-placeholder">
<Typography.Paragraph> <Typography.Paragraph>
<Transi18next <Transi18next
i18nKey="message.view-sample-data-entity" i18nKey="message.view-sample-data-entity"
@ -142,6 +235,30 @@ const SampleDataTable = ({ isTableDeleted, tableId }: SampleDataProps) => {
})} })}
data-testid="sample-data" data-testid="sample-data"
id="sampleDataDetails"> 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 <AntdTable
bordered bordered
columns={sampleData?.columns} columns={sampleData?.columns}
@ -152,8 +269,20 @@ const SampleDataTable = ({ isTableDeleted, tableId }: SampleDataProps) => {
scroll={{ x: true }} scroll={{ x: true }}
size="small" 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> </div>
); );
}; };
export default withLoader<SampleDataProps>(SampleDataTable); export default withLoader<SampleDataProps>(observer(SampleDataTable));

View File

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

View File

@ -12,11 +12,27 @@
*/ */
import { act, render, screen } from '@testing-library/react'; 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 React from 'react';
import { getSampleDataByTableId } from 'rest/tableAPI'; import { getSampleDataByTableId } from 'rest/tableAPI';
import { MOCK_TABLE } from '../../mocks/TableData.mock'; import { MOCK_TABLE } from '../../mocks/TableData.mock';
import SampleDataTable from './SampleDataTable.component'; 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', () => ({ jest.mock('react-router-dom', () => ({
Link: jest.fn().mockImplementation(({ children }) => <span>{children}</span>), Link: jest.fn().mockImplementation(({ children }) => <span>{children}</span>),
useLocation: jest.fn().mockImplementation(() => ({ pathname: 'test' })), 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', () => { describe('Test SampleDataTable Component', () => {
it('Render error placeholder if the columns passed are empty', async () => { it('Render error placeholder if the columns passed are empty', async () => {
(getSampleDataByTableId as jest.Mock).mockImplementationOnce(() => (getSampleDataByTableId as jest.Mock).mockImplementationOnce(() =>
@ -43,7 +63,7 @@ describe('Test SampleDataTable Component', () => {
); );
await act(async () => { await act(async () => {
render(<SampleDataTable tableId="id" />); render(<SampleDataTable {...mockProps} />);
}); });
const errorPlaceholder = screen.getByTestId('error-placeholder'); 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 () => { it('Renders all the data that was sent to the component', async () => {
await act(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'); const table = screen.getByTestId('sample-data-table');
expect(deleteButton).toBeInTheDocument();
expect(table).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. * limitations under the License.
*/ */
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface';
export type SampleDataType = export type SampleDataType =
| string | string
@ -29,4 +30,6 @@ export interface SampleData {
export interface SampleDataProps { export interface SampleDataProps {
isTableDeleted?: boolean; isTableDeleted?: boolean;
tableId: string; tableId: string;
ownerId: string;
permissions: OperationPermission;
} }

View File

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

View File

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

View File

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

View File

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