diff --git a/openmetadata-ui/src/main/resources/ui/package.json b/openmetadata-ui/src/main/resources/ui/package.json index 964eb9ccede..63fde47b403 100644 --- a/openmetadata-ui/src/main/resources/ui/package.json +++ b/openmetadata-ui/src/main/resources/ui/package.json @@ -74,6 +74,8 @@ "react-codemirror2": "^7.2.1", "react-context-mutex": "^2.0.0", "react-copy-to-clipboard": "^5.0.4", + "react-dnd": "14.0.2", + "react-dnd-html5-backend": "14.0.2", "react-dom": "^16.14.0", "react-error-boundary": "^3.1.4", "react-i18next": "^11.18.6", @@ -181,6 +183,7 @@ "connect-api-mocker": "^1.10.0", "copy-webpack-plugin": "^7.0.0", "css-loader": "^6.7.2", + "cypress-postgresql": "^1.0.8", "dotenv": "^16.0.0", "eslint": "^6.6.0", "eslint-config-prettier": "^6.11.0", @@ -219,7 +222,6 @@ "webpack-bundle-analyzer": "^4.4.0", "webpack-cli": "^4.3.1", "webpack-dev-server": "^4.11.1", - "webpackbar": "^5.0.0-3", - "cypress-postgresql": "^1.0.8" + "webpackbar": "^5.0.0-3" } } diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/drag.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/drag.svg new file mode 100644 index 00000000000..468fed60f5a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/drag.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/dashboardAPI.ts b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/dashboardAPI.ts index b221a12ae0d..1da1ac51e03 100644 --- a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/dashboardAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/dashboardAPI.ts @@ -13,7 +13,7 @@ import { AxiosResponse } from 'axios'; import { Operation } from 'fast-json-patch'; -import { RestoreEntitiesRequestType } from 'Models'; +import { RestoreRequestType } from 'Models'; import { Dashboard } from '../generated/entity/data/dashboard'; import { EntityHistory } from '../generated/type/entityHistory'; import { EntityReference } from '../generated/type/entityReference'; @@ -122,7 +122,7 @@ export const patchDashboardDetails = async (id: string, data: Operation[]) => { export const restoreDashboard = async (id: string) => { const response = await APIClient.put< - RestoreEntitiesRequestType, + RestoreRequestType, AxiosResponse >('/dashboards/restore', { id }); diff --git a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/pipelineAPI.ts b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/pipelineAPI.ts index 4ce1f7c59af..933960162bb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/pipelineAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/pipelineAPI.ts @@ -13,7 +13,7 @@ import { AxiosResponse } from 'axios'; import { Operation } from 'fast-json-patch'; -import { PagingResponse, RestoreEntitiesRequestType } from 'Models'; +import { PagingResponse, RestoreRequestType } from 'Models'; import { Pipeline, PipelineStatus } from '../generated/entity/data/pipeline'; import { EntityHistory } from '../generated/type/entityHistory'; import { EntityReference } from '../generated/type/entityReference'; @@ -140,7 +140,7 @@ export const getPipelineStatus = async ( export const restorePipeline = async (id: string) => { const response = await APIClient.put< - RestoreEntitiesRequestType, + RestoreRequestType, AxiosResponse >('/pipelines/restore', { id, diff --git a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/tableAPI.ts b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/tableAPI.ts index 142009de9b4..c455ee495a8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/tableAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/tableAPI.ts @@ -13,7 +13,7 @@ import { AxiosResponse } from 'axios'; import { Operation } from 'fast-json-patch'; -import { RestoreEntitiesRequestType } from 'Models'; +import { RestoreRequestType } from 'Models'; import { ColumnProfile, Table, @@ -98,7 +98,7 @@ export const patchTableDetails = async (id: string, data: Operation[]) => { export const restoreTable = async (id: string) => { const response = await APIClient.put< - RestoreEntitiesRequestType, + RestoreRequestType, AxiosResponse >('/tables/restore', { id }); diff --git a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/teamsAPI.ts b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/teamsAPI.ts index 9b8716b24bf..f5a774a5204 100644 --- a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/teamsAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/teamsAPI.ts @@ -14,6 +14,7 @@ import { AxiosResponse } from 'axios'; import { Operation } from 'fast-json-patch'; import { isString } from 'lodash'; +import { RestoreRequestType } from 'Models'; import { CreateTeam } from '../generated/api/teams/createTeam'; import { Team } from '../generated/entity/teams/team'; import { TeamHierarchy } from '../generated/entity/teams/teamHierarchy'; @@ -102,7 +103,7 @@ export const deleteTeam = async (id: string) => { return response.data; }; -export const reactivateTeam = async (data: CreateTeam) => { +export const updateTeam = async (data: CreateTeam) => { const response = await APIClient.put>( '/teams', data @@ -110,3 +111,12 @@ export const reactivateTeam = async (data: CreateTeam) => { return response.data; }; + +export const restoreTeam = async (id: string) => { + const response = await APIClient.put>( + '/teams/restore', + { id } + ); + + return response.data; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/topicsAPI.ts b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/topicsAPI.ts index 74c8c429dac..eb32dcc1895 100644 --- a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/topicsAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/topicsAPI.ts @@ -13,7 +13,7 @@ import { AxiosResponse } from 'axios'; import { Operation } from 'fast-json-patch'; -import { RestoreEntitiesRequestType } from 'Models'; +import { RestoreRequestType } from 'Models'; import { TabSpecificField } from '../enums/entity.enum'; import { Topic } from '../generated/entity/data/topic'; import { EntityHistory } from '../generated/type/entityHistory'; @@ -128,7 +128,7 @@ export const patchTopicDetails = async (id: string, data: Operation[]) => { export const restoreTopic = async (id: string) => { const response = await APIClient.put< - RestoreEntitiesRequestType, + RestoreRequestType, AxiosResponse >('/topics/restore', { id }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/DraggableBodyRow.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/DraggableBodyRow.tsx new file mode 100644 index 00000000000..0ba4d405187 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/DraggableBodyRow.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2022 Collate + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useRef } from 'react'; +import { useDrag, useDrop } from 'react-dnd'; +import { DRAGGABLE_BODY_ROW } from '../../constants/Teams.constants'; +import { Team } from '../../generated/entity/teams/team'; +import { DragCollectProps, DraggableBodyRowProps } from './team.interface'; + +const DraggableBodyRow = ({ + index, + handleMoveRow, + className, + record, + style, + ...restProps +}: DraggableBodyRowProps) => { + const ref = useRef(null); + const [{ isOver, dropClassName }, drop] = useDrop({ + accept: DRAGGABLE_BODY_ROW, + collect: (monitor: DragCollectProps) => { + const { index: dragIndex } = monitor?.getItem() || {}; + if (dragIndex === index) { + return {}; + } + + return { + isOver: monitor.isOver(), + dropClassName: + dragIndex < index ? ' drop-over-downward' : ' drop-over-upward', + }; + }, + // this will going to return the drag and drop object of a table + drop: ({ record: dragRecord }: { record: Team }) => { + handleMoveRow(dragRecord, record); + }, + }); + // here we are passing the drag record + const [, drag] = useDrag({ + type: DRAGGABLE_BODY_ROW, + item: { record }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }); + drop(drag(ref)); + + return ( + + ); +}; + +export default DraggableBodyRow; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/TeamDetailsV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/TeamDetailsV1.tsx index 59f8ccfaae1..63ec99e9598 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/TeamDetailsV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/TeamDetailsV1.tsx @@ -36,7 +36,7 @@ import React, { Fragment, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import AppState from '../../AppState'; -import { reactivateTeam } from '../../axiosAPIs/teamsAPI'; +import { restoreTeam } from '../../axiosAPIs/teamsAPI'; import { getTeamAndUserDetailsPath, getUserPath, @@ -76,7 +76,6 @@ import SVGIcons, { Icons } from '../../utils/SvgUtils'; import { filterChildTeams, getDeleteMessagePostFix, - getRestoreTeamData, } from '../../utils/TeamUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; import { Button } from '../buttons/Button/Button'; @@ -514,9 +513,7 @@ const TeamDetailsV1 = ({ const handleReactiveTeam = async () => { try { - const res = await reactivateTeam( - getRestoreTeamData(currentTeam, childTeams) - ); + const res = await restoreTeam(currentTeam.id); if (res) { afterDeleteAction(); showSuccessToast( @@ -1166,7 +1163,7 @@ const TeamDetailsV1 = ({ ) : ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/TeamHierarchy.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/TeamHierarchy.test.tsx new file mode 100644 index 00000000000..b24397d9ba2 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/TeamHierarchy.test.tsx @@ -0,0 +1,119 @@ +/* + * Copyright 2022 Collate + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { act, fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { MOCK_CURRENT_TEAM, MOCK_TABLE_DATA } from '../../mocks/Teams.mock'; +import { TeamHierarchyProps } from './team.interface'; +import TeamHierarchy from './TeamHierarchy'; + +const teamHierarchyPropsData: TeamHierarchyProps = { + data: MOCK_TABLE_DATA, + currentTeam: MOCK_CURRENT_TEAM, + onTeamExpand: jest.fn(), +}; + +const mockShowErrorToast = jest.fn(); + +// mock library imports +jest.mock('react-router-dom', () => ({ + Link: jest + .fn() + .mockImplementation(({ children }) => {children}), +})); + +jest.mock('../../utils/TeamUtils', () => ({ + getMovedTeamData: jest.fn().mockReturnValue([]), +})); + +jest.mock('../../axiosAPIs/teamsAPI', () => ({ + updateTeam: jest + .fn() + .mockImplementation(() => Promise.resolve(MOCK_CURRENT_TEAM)), + getTeamByName: jest + .fn() + .mockImplementation(() => Promise.resolve(MOCK_CURRENT_TEAM)), +})); + +jest.mock('../../utils/CommonUtils', () => ({ + getEntityName: jest.fn().mockReturnValue('entityName'), +})); + +jest.mock('../../utils/RouterUtils', () => ({ + getTeamsWithFqnPath: jest.fn().mockReturnValue([]), +})); + +jest.mock('../../utils/ToastUtils', () => ({ + showErrorToast: jest.fn().mockImplementation(() => mockShowErrorToast), +})); + +describe('Team Hierarchy page', () => { + it('Initially, Table should load', async () => { + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + const table = await screen.findByTestId('team-hierarchy-table'); + + expect(table).toBeInTheDocument(); + }); + + it('Should render all table columns', async () => { + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + const table = await screen.findByTestId('team-hierarchy-table'); + const teamsColumn = await screen.findByText('Teams'); + const typeColumn = await screen.findByText('Type'); + const subTeamsColumn = await screen.findByText('Sub Teams'); + const usersColumn = await screen.findByText('Users'); + const assetCountColumn = await screen.findByText('Asset Count'); + const descriptionColumn = await screen.findByText('Description'); + const rows = await screen.findAllByRole('row'); + + expect(table).toBeInTheDocument(); + expect(teamsColumn).toBeInTheDocument(); + expect(typeColumn).toBeInTheDocument(); + expect(subTeamsColumn).toBeInTheDocument(); + expect(usersColumn).toBeInTheDocument(); + expect(assetCountColumn).toBeInTheDocument(); + expect(descriptionColumn).toBeInTheDocument(); + + expect(rows).toHaveLength(MOCK_TABLE_DATA.length + 1); + }); + + it('Should render child row in table', async () => { + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + const table = await screen.findByTestId('team-hierarchy-table'); + + expect(table).toBeInTheDocument(); + + const expandableTableRow = await screen.getAllByTestId('expand-table-row'); + fireEvent.click(expandableTableRow[0]); + + const totalRows = await screen.findAllByText('entityName'); + + expect(totalRows).toHaveLength(5); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/TeamHierarchy.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/TeamHierarchy.tsx index e491388ccce..5c8504d7d9a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/TeamHierarchy.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/TeamHierarchy.tsx @@ -11,27 +11,42 @@ * limitations under the License. */ -import { Table } from 'antd'; +import { Modal, Table, Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; -import { isEmpty } from 'lodash'; -import React, { FC, useMemo } from 'react'; +import { ExpandableConfig } from 'antd/lib/table/interface'; +import { AxiosError } from 'axios'; +import { isArray, isEmpty } from 'lodash'; +import React, { FC, useCallback, useMemo, useState } from 'react'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; +import { getTeamByName, updateTeam } from '../../axiosAPIs/teamsAPI'; +import { TABLE_CONSTANTS } from '../../constants/Teams.constants'; import { Team } from '../../generated/entity/teams/team'; import { getEntityName } from '../../utils/CommonUtils'; import { getTeamsWithFqnPath } from '../../utils/RouterUtils'; import SVGIcons, { Icons } from '../../utils/SvgUtils'; +import { getMovedTeamData } from '../../utils/TeamUtils'; +import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; +import { + DraggableBodyRowProps, + MovedTeamProps, + TableExpandableDataProps, + TeamHierarchyProps, +} from './team.interface'; import './teams.less'; -interface TeamHierarchyProps { - data: Team[]; - onTeamExpand: ( - loading?: boolean, - parentTeam?: string, - updateChildNode?: boolean - ) => void; -} +const TeamHierarchy: FC = ({ + currentTeam, + data, + onTeamExpand, +}) => { + const { t } = useTranslation(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isTableLoading, setIsTableLoading] = useState(false); + const [movedTeam, setMovedTeam] = useState(); -const TeamHierarchy: FC = ({ data, onTeamExpand }) => { const columns: ColumnsType = useMemo(() => { return [ { @@ -78,42 +93,137 @@ const TeamHierarchy: FC = ({ data, onTeamExpand }) => { ]; }, [data, onTeamExpand]); - return ( -
- expandable ? ( - - onExpand( - record, - e as unknown as React.MouseEvent - ) - }> - - - ) : ( -
- ), - }} - pagination={false} - rowKey="name" - size="small" - onExpand={(isOpen, record) => { + const handleMoveRow = useCallback( + async (dragRecord: Team, dropRecord: Team) => { + if (dragRecord.id === dropRecord.id) { + return; + } + let dropTeam: Team = dropRecord; + if (!isArray(dropTeam.children)) { + const res = await getTeamByName(dropTeam.name, ['parents'], 'all'); + dropTeam = (res.parents?.[0] as Team) || currentTeam; + } + setMovedTeam({ + from: dragRecord, + to: dropTeam, + }); + setIsModalOpen(true); + }, + [] + ); + + const handleChangeTeam = async () => { + if (movedTeam) { + setIsTableLoading(true); + try { + const data = await getTeamByName( + movedTeam.from.name, + ['users', 'defaultRoles', 'policies', 'owner', 'parents', 'children'], + 'all' + ); + await updateTeam(getMovedTeamData(data, [movedTeam.to.id])); + onTeamExpand(true, currentTeam?.name); + showSuccessToast(t('message.team-moved-success')); + } catch (error) { + showErrorToast(error as AxiosError, t('server.team-moved-error')); + } finally { + setIsTableLoading(false); + setIsModalOpen(false); + } + } + }; + + const tableExpandableIconData = useMemo( + () => + ({ expanded, onExpand, expandable, record }: TableExpandableDataProps) => + expandable ? ( +
+ onExpand( + record, + e as unknown as React.MouseEvent + ) + }> + + +
+ ) : ( + <> + +
+ + ), + [] + ); + + const expandableConfig: ExpandableConfig = useMemo( + () => ({ + onExpand: (isOpen, record) => { if (isOpen && isEmpty(record.children)) { onTeamExpand(false, record.fullyQualifiedName, true); } - }} - /> + }, + expandIcon: ({ expanded, onExpand, expandable, record }) => + tableExpandableIconData({ expanded, onExpand, expandable, record }), + }), + [onTeamExpand, tableExpandableIconData] + ); + + return ( + <> + +
{ + const attr = { + index, + handleMoveRow, + record, + }; + + return attr as DraggableBodyRowProps; + }} + /> + + + setIsModalOpen(false)} + onOk={handleChangeTeam}> + + {t('message.team-transfer-message', { + from: movedTeam?.from?.name, + to: movedTeam?.to?.name, + })} + + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/team.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/team.interface.ts new file mode 100644 index 00000000000..9fcfc181e9f --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/team.interface.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2022 Collate + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Team } from '../../generated/entity/teams/team'; + +export interface TeamHierarchyProps { + currentTeam?: Team; + data: Team[]; + onTeamExpand: ( + loading?: boolean, + parentTeam?: string, + updateChildNode?: boolean + ) => void; +} + +export interface DraggableBodyRowProps + extends React.HTMLAttributes { + index: number; + handleMoveRow: (dragRecord: Team, dropRecord: Team) => void; + record: Team; +} + +export interface MovedTeamProps { + from: Team; + to: Team; +} + +export interface DragCollectProps { + getItem: () => { index: number }; + isOver: (options?: { shallow?: boolean }) => boolean; +} + +export interface TableExpandableDataProps { + expanded: boolean; + onExpand: (record: Team, event: React.MouseEvent) => void; + expandable: boolean; + record: Team; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/teams.less b/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/teams.less index aec8dc2c828..1d9d63e1416 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/teams.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/TeamDetails/teams.less @@ -54,16 +54,40 @@ td { border-right: none; background: @white; - margin-right: 10px; + + .drag-icon { + width: 12px; + height: 12px; + margin-right: 6px; + display: inline-flex; + visibility: hidden; + } + + .expand-icon { + cursor: pointer; + } .expand-cell-icon-container { + display: inline-flex; + margin-right: 8px; + } + + .expand-cell-empty-icon-container { width: 16px; height: 16px; display: inline-flex; } } + + .ant-table-cell-with-append { + display: flex; + align-items: center; + } .ant-table-cell-row-hover { background: @body-dark-bg-color; + .drag-icon { + visibility: initial; + } } } } diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Teams.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Teams.constants.ts new file mode 100644 index 00000000000..ca5940bd6a6 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Teams.constants.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2022 Collate + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import DraggableBodyRow from '../components/TeamDetails/DraggableBodyRow'; + +export const DRAGGABLE_BODY_ROW = 'DraggableBodyRow'; + +export const TABLE_CONSTANTS = { + body: { + row: DraggableBodyRow, + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts b/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts index d04f25f6d5d..ddd3187d8ad 100644 --- a/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts +++ b/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts @@ -17,7 +17,7 @@ declare module 'Models' { import { TagLabel } from '../generated/type/tagLabel'; import { Paging } from './../generated/type/paging'; - export interface RestoreEntitiesRequestType { + export interface RestoreRequestType { id: string; } diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index e07832e3f14..00211a12814 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -381,6 +381,7 @@ "hide": "Hide", "restore-team": "Restore Team", "remove": "Remove", + "move-the-team": "Move the Team", "data-insight-plural": "Data Insights", "configure-entity": "Configure {{entity}}", "name-lowercase": "name", @@ -536,6 +537,8 @@ "delete-message-question-mark": "Delete Message?", "view-deleted-teams": "View all the Deleted Teams, which come under this Team.", "restore-deleted-team": " Restoring the Team will add all the metadata back to OpenMetadata", + "team-moved-success": "Team moved successfully", + "team-transfer-message": "Click on Confirm if you’d like to move {{from}} team under {{to}} team.", "create-new-glossary-guide": "A Glossary is a controlled vocabulary used to define the concepts and terminology in an organization. Glossaries can be specific to a certain domain (for e.g., Business Glossary, Technical Glossary). In the glossary, the standard terms and concepts can be defined along with the synonyms, and related terms. Control can be established over how and who can add the terms in the glossary.", "no-notification-found": "No notifications Found", "enables-end-to-end-metadata-management": "Enables end-to-end metadata management with data discovery, data duality, observability, and people collaboration", @@ -573,7 +576,8 @@ "leave-team-error": "Error while leaving the team!", "no-query-available": "No query available", "unexpected-response": "Unexpected response from server!", - "ingestion-workflow-operation-error": "Error while {{operation}} ingestion workflow {{displayName}}" + "ingestion-workflow-operation-error": "Error while {{operation}} ingestion workflow {{displayName}}", + "team-moved-error": "Error while moving team" }, "url": {} } diff --git a/openmetadata-ui/src/main/resources/ui/src/mocks/Teams.mock.ts b/openmetadata-ui/src/main/resources/ui/src/mocks/Teams.mock.ts new file mode 100644 index 00000000000..bdfeb03eada --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/mocks/Teams.mock.ts @@ -0,0 +1,191 @@ +/* Copyright 2022 Collate + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const MOCK_CURRENT_TEAM = { + childrenCount: 22, + defaultRoles: [ + { + deleted: false, + description: + 'Users with Data Consumer role use different data assets for their day to day work.', + displayName: 'Data Consumer', + fullyQualifiedName: 'DataConsumer', + href: 'http://sandbox-beta.open-metadata.org/api/v1/roles/1497b0cf-cb5f-42c2-8e13-3ab68b90bfa0', + id: '1497b0cf-cb5f-42c2-8e13-3ab68b90bfa0', + name: 'DataConsumer', + type: 'role', + }, + ], + deleted: false, + description: + 'Organization under which all the other team hierarchy is created', + displayName: 'Organization', + fullyQualifiedName: 'Organization', + href: 'http://sandbox-beta.open-metadata.org/api/v1/teams/f9578f16-363a-4788-80fb-d05816c9e169', + id: 'f9578f16-363a-4788-80fb-d05816c9e169', + inheritedRoles: [], + isJoinable: false, + name: 'Organization', + owns: [], + parents: [], + policies: [ + { + deleted: false, + description: 'Policy for all the users of an organization.', + displayName: 'Organization Policy', + fullyQualifiedName: 'OrganizationPolicy', + href: 'http://sandbox-beta.open-metadata.org/api/v1/policies/09f4480c-ef57-4239-b2aa-c87053ad4f46', + id: '09f4480c-ef57-4239-b2aa-c87053ad4f46', + name: 'OrganizationPolicy', + type: 'policy', + }, + ], + teamType: undefined, + updatedAt: 1669719624263, + updatedBy: 'ag939431', + users: [], + version: 2.4, +}; + +export const MOCK_TABLE_DATA = [ + { + children: [ + { + children: undefined, + childrenCount: 0, + defaultRoles: [], + deleted: false, + fullyQualifiedName: 'Applications', + href: 'http://localhost:8585/api/v1/teams/eb4b1b74-d30e-4bfa-8409-dac15db3cc32', + id: 'eb4b1b74-d30e-4bfa-8409-dac15db3cc32', + inheritedRoles: [], + isJoinable: true, + key: 'Applications', + name: 'Applications', + owns: [], + teamType: 'Group', + updatedAt: 1670390160760, + updatedBy: 'admin', + userCount: 12, + version: 0.1, + type: 'BusinessUnit', + }, + { + children: undefined, + childrenCount: 3, + defaultRoles: [], + deleted: false, + fullyQualifiedName: 'Infrastructure', + href: 'http://localhost:8585/api/v1/teams/c8cc8922-8917-4d33-94e3-d9d257dd8830', + id: 'c8cc8922-8917-4d33-94e3-d9d257dd8830', + inheritedRoles: [], + isJoinable: true, + key: 'Infrastructure', + name: 'Infrastructure', + owns: [], + teamType: 'BusinessUnit', + type: 'BusinessUnit', + updatedAt: 1670390159742, + updatedBy: 'admin', + userCount: 20, + version: 0.1, + }, + ], + childrenCount: 4, + defaultRoles: [], + deleted: false, + fullyQualifiedName: 'Engineering', + href: 'http://sandbox-beta.open-metadata.org/api/v1/teams/49d060a2-ad14-48a7-840a-836cd99aaffb', + id: '49d060a2-ad14-48a7-840a-836cd99aaffb', + inheritedRoles: [ + { + deleted: false, + description: + 'Users with Data Consumer role use different data assets for their day to day work.', + displayName: 'Data Consumer', + fullyQualifiedName: 'DataConsumer', + href: 'http://sandbox-beta.open-metadata.org/api/v1/roles/1497b0cf-cb5f-42c2-8e13-3ab68b90bfa0', + id: '1497b0cf-cb5f-42c2-8e13-3ab68b90bfa0', + name: 'DataConsumer', + type: 'role', + }, + ], + isJoinable: true, + key: 'Engineering', + name: 'Engineering', + owns: [], + teamType: undefined, + updatedAt: 1670312015218, + updatedBy: 'ingestion-bot', + userCount: 50, + }, + { + children: [], + childrenCount: 3, + defaultRoles: [], + deleted: false, + fullyQualifiedName: 'Finance', + href: 'http://sandbox-beta.open-metadata.org/api/v1/teams/b201a5b2-b0e8-461d-9fa1-cd5212d09eee', + id: 'b201a5b2-b0e8-461d-9fa1-cd5212d09eee', + inheritedRoles: [ + { + deleted: false, + description: + 'Users with Data Consumer role use different data assets for their day to day work.', + displayName: 'Data Consumer', + fullyQualifiedName: 'DataConsumer', + href: 'http://sandbox-beta.open-metadata.org/api/v1/roles/1497b0cf-cb5f-42c2-8e13-3ab68b90bfa0', + id: '1497b0cf-cb5f-42c2-8e13-3ab68b90bfa0', + name: 'DataConsumer', + type: 'role', + }, + ], + isJoinable: true, + key: 'Finance', + name: 'Finance', + owns: [], + teamType: undefined, + updatedAt: 1670312016093, + updatedBy: 'ingestion-bot', + userCount: 2, + }, + { + children: [], + childrenCount: 2, + defaultRoles: [], + deleted: false, + fullyQualifiedName: 'Legal', + href: 'http://sandbox-beta.open-metadata.org/api/v1/teams/e64afbd0-aab5-4aed-952d-c5a5b8ba06bb', + id: 'e64afbd0-aab5-4aed-952d-c5a5b8ba06bb', + inheritedRoles: [ + { + deleted: false, + description: + 'Users with Data Consumer role use different data assets for their day to day work.', + displayName: 'Legal', + fullyQualifiedName: 'Legal', + href: 'http://sandbox-beta.open-metadata.org/api/v1/roles/1497b0cf-cb5f-42c2-8e13-3ab68b90bfa0', + id: '1497b0cf-cb5f-42c2-8e13-3ab68b90bfa0', + name: 'Legal', + type: 'role', + }, + ], + isJoinable: true, + key: 'Marketing', + name: 'Marketing', + owns: [], + teamType: undefined, + updatedAt: 1670312016516, + updatedBy: 'ingestion-bot', + userCount: 3, + }, +]; diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/position.less b/openmetadata-ui/src/main/resources/ui/src/styles/position.less index 8e75de3c31e..8106cf6791a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/position.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/position.less @@ -32,6 +32,9 @@ .d-flex { display: flex; } +.d-inline-flex { + display: inline-flex; +} .inline { display: inline; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx index 949bdb21872..abd3f273ff1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx @@ -63,6 +63,7 @@ import IconDeployIngestion from '../assets/svg/deploy-ingestion.svg'; import IconDocPrimary from '../assets/svg/doc-primary.svg'; import IconDocWhite from '../assets/svg/doc-white.svg'; import IconDoc from '../assets/svg/doc.svg'; +import IconDrag from '../assets/svg/drag.svg'; import IconEditBlack from '../assets/svg/edit-black.svg'; import IconEditOutlinePrimary from '../assets/svg/edit-outline-primery.svg'; import IconEditPrimary from '../assets/svg/edit-primary.svg'; @@ -397,6 +398,7 @@ export const Icons = { HIDE_PASSWORD: 'hide-password', ARROW_RIGHT_LIGHT: 'arrow-right-light', ARROW_DOWN_LIGHT: 'arrow-down-light', + DRAG: 'drag', }; const SVGIcons: FunctionComponent = ({ icon, ...props }: Props) => { @@ -509,6 +511,11 @@ const SVGIcons: FunctionComponent = ({ icon, ...props }: Props) => { case Icons.FEED: IconComponent = IconFeed; + break; + + case Icons.DRAG: + IconComponent = IconDrag; + break; case Icons.THUMBSUP: IconComponent = IconThumbsUp; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TeamUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TeamUtils.ts index 478025d3bf7..ab2219bf277 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TeamUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TeamUtils.ts @@ -12,7 +12,7 @@ */ import { t } from 'i18next'; -import { cloneDeep, isEmpty, isNil, isUndefined, omit } from 'lodash'; +import { cloneDeep, isNil, isUndefined, omit } from 'lodash'; import { CreateTeam } from '../generated/api/teams/createTeam'; import { EntityReference, @@ -53,10 +53,7 @@ const getEntityValue = (value: EntityReference[] | undefined) => { return undefined; }; -export const getRestoreTeamData = ( - team: Team, - childTeams: Team[] -): CreateTeam => { +export const getMovedTeamData = (team: Team, parents: string[]): CreateTeam => { const userDetails = omit(cloneDeep(team), [ 'id', 'fullyQualifiedName', @@ -70,18 +67,20 @@ export const getRestoreTeamData = ( 'changeDescription', 'deleted', 'inheritedRoles', + 'key', ]) as Team; - const { parents, policies, users, defaultRoles } = userDetails; + const { policies, users, defaultRoles, children } = userDetails; return { ...userDetails, teamType: userDetails.teamType as TeamType, defaultRoles: getEntityValue(defaultRoles), - children: isEmpty(childTeams) - ? undefined - : getEntityIdArray(childTeams as EntityReference[]), - parents: getEntityValue(parents), + children: + userDetails.teamType == TeamType.Group + ? undefined + : getEntityValue(children), + parents: parents, policies: getEntityValue(policies), users: getEntityValue(users), }; diff --git a/openmetadata-ui/src/main/resources/ui/yarn.lock b/openmetadata-ui/src/main/resources/ui/yarn.lock index d5e6b63c6f3..18913f11d5b 100644 --- a/openmetadata-ui/src/main/resources/ui/yarn.lock +++ b/openmetadata-ui/src/main/resources/ui/yarn.lock @@ -2856,6 +2856,21 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.15.tgz#6a9d143f7f4f49db2d782f9e1c8839a29b43ae23" integrity sha512-15spi3V28QdevleWBNXE4pIls3nFZmBbUGrW9IVPwiQczuSb9n76TCB4bsk8TSel+I1OkHEdPhu5QKMfY6rQHA== +"@react-dnd/asap@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab" + integrity sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg== + +"@react-dnd/invariant@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-2.0.0.tgz#09d2e81cd39e0e767d7da62df9325860f24e517e" + integrity sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw== + +"@react-dnd/shallowequal@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a" + integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg== + "@rc-component/portal@^1.0.0-6", "@rc-component/portal@^1.0.0-8": version "1.0.3" resolved "https://registry.yarnpkg.com/@rc-component/portal/-/portal-1.0.3.tgz#3aa2c229a7a20ac2412d864e8977e6377973416e" @@ -6511,6 +6526,24 @@ dlv@^1.1.3: resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== +dnd-core@14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.0.tgz#973ab3470d0a9ac5a0fa9021c4feba93ad12347d" + integrity sha512-wTDYKyjSqWuYw3ZG0GJ7k+UIfzxTNoZLjDrut37PbcPGNfwhlKYlPUqjAKUjOOv80izshUiqusaKgJPItXSevA== + dependencies: + "@react-dnd/asap" "^4.0.0" + "@react-dnd/invariant" "^2.0.0" + redux "^4.0.5" + +dnd-core@14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.1.tgz#76d000e41c494983210fb20a48b835f81a203c2e" + integrity sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A== + dependencies: + "@react-dnd/asap" "^4.0.0" + "@react-dnd/invariant" "^2.0.0" + redux "^4.1.1" + dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -12417,6 +12450,24 @@ react-copy-to-clipboard@^5.0.4: copy-to-clipboard "^3" prop-types "^15.5.8" +react-dnd-html5-backend@14.0.2: + version "14.0.2" + resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.2.tgz#25019388f6abdeeda3a6fea835dff155abb2085c" + integrity sha512-QgN6rYrOm4UUj6tIvN8ovImu6uP48xBXF2rzVsp6tvj6d5XQ7OjHI4SJ/ZgGobOneRAU3WCX4f8DGCYx0tuhlw== + dependencies: + dnd-core "14.0.1" + +react-dnd@14.0.2: + version "14.0.2" + resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.2.tgz#57266baec92b887301f81fa3b77f87168d159733" + integrity sha512-JoEL78sBCg8SzjOKMlkR70GWaPORudhWuTNqJ56lb2P8Vq0eM2+er3ZrMGiSDhOmzaRPuA9SNBz46nHCrjn11A== + dependencies: + "@react-dnd/invariant" "^2.0.0" + "@react-dnd/shallowequal" "^2.0.0" + dnd-core "14.0.0" + fast-deep-equal "^3.1.3" + hoist-non-react-statics "^3.3.2" + react-dom@^16.14.0: version "16.14.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" @@ -12823,7 +12874,7 @@ reduce-css-calc@^2.1.8: css-unit-converter "^1.1.1" postcss-value-parser "^3.3.0" -redux@^4.0.0, redux@^4.1.0: +redux@^4.0.0, redux@^4.0.5, redux@^4.1.0, redux@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==