diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-delete.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-delete.svg index 1623ebb486d..af5c84f5c2b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-delete.svg +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-delete.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/SampleDataTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/SampleDataTable.component.tsx index b40b437b58c..6a981d7921a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/SampleDataTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/SampleDataTable.component.tsx @@ -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(); const [isLoading, setIsLoading] = useState(true); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(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: ( + + } + 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 ( - + { })} data-testid="sample-data" id="sampleDataDetails"> + + {hasPermission && ( + + + + + + )} + + { scroll={{ x: true }} size="small" /> + + {isDeleteModalOpen && ( + + )} ); }; -export default withLoader(SampleDataTable); +export default withLoader(observer(SampleDataTable)); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/SampleDataTable.style.less b/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/SampleDataTable.style.less index 64e366e36e6..b55eceefb88 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/SampleDataTable.style.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/SampleDataTable.style.less @@ -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); } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/SampleDataTable.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/SampleDataTable.test.tsx index 7e08d616946..8d8b46c34c4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/SampleDataTable.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/SampleDataTable.test.tsx @@ -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 }) => {children}), 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(

EntityDeleteModal

); +}); + 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(); + render(); }); 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(); + render(); }); + 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( + + ); + }); + + 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(); + }); + + 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(); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/sample.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/sample.interface.ts index 535af7637d3..cfb3efe9d1f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/sample.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/SampleDataTable/sample.interface.ts @@ -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; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/UserPopOverCard.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/UserPopOverCard.tsx index db64222532a..e62abb61307 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/UserPopOverCard.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/UserPopOverCard.tsx @@ -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 = ({ children, userName, type = 'user' }) => { const [isLoading, setIsLoading] = useState(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 = ({ children, userName, type = 'user' }) => { const teams = getNonDeletedTeams(userData.teams ?? []); return teams?.length ? ( -

- - - {t('label.team-plural')} - - - {teams.map((team, i) => ( +

+

+ + + {t('label.team-plural')} + +

+ +

+ {teams.map((team) => ( + className="bg-grey rounded-4 p-x-xs text-grey-body text-xs m-b-xss" + key={team.id}> {team?.displayName ?? team?.name} ))} - -

+

+
) : null; }; @@ -91,24 +95,29 @@ const UserPopOverCard: FC = ({ children, userName, type = 'user' }) => { const isAdmin = userData?.isAdmin; return roles?.length ? ( -

- - - {t('label.role-plural')} - +

+

+ + + {t('label.role-plural')} + +

+ {isAdmin && ( - + {TERM_ADMIN} )} - {roles.map((role, i) => ( - + {roles.map((role) => ( + {role?.displayName ?? role?.name} ))} -

+
) : null; }; @@ -117,25 +126,24 @@ const UserPopOverCard: FC = ({ children, userName, type = 'user' }) => { const displayName = getEntityName(userData as unknown as EntityReference); return ( -
-
- -
+ +
- + {displayName !== name ? ( {name} ) : null} {isEmpty(userData) && {userName}}
-
+ ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts index 3b1dd958a21..b2ad0e6dc32 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts @@ -47,6 +47,7 @@ export enum EntityType { SUBSCRIPTION = 'subscription', USER_NAME = 'username', CHART = 'chart', + SAMPLE_DATA = 'sampleData', } export enum AssetsType { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index a6b9218a137..47dcdcd7e23 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -563,6 +563,8 @@ const TableDetailsPageV1 = () => { ) : ( ), diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/tableAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/tableAPI.ts index ddd19453bf7..a371d546608 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/tableAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/tableAPI.ts @@ -246,3 +246,7 @@ export const getTableList = async (params?: TableListParams) => { return response.data; }; + +export const deleteSampleDataByTableId = async (id: string) => { + return await APIClient.delete(`/tables/${id}/sampleData`); +};