From 2f1efc1fdff0cdd77972b479d3d5a12ad300c204 Mon Sep 17 00:00:00 2001 From: radekkaluzik <51480040+radekkaluzik@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:01:31 +0100 Subject: [PATCH] feat: add detailed view to Workspaces (#1682) --- package-lock.json | 114 +- package.json | 2 +- src/Messages.js | 40 + src/Routing.tsx | 9 +- src/helpers/workspaces/api.js | 1 - src/helpers/workspaces/workspaces-helper.ts | 4 + src/redux/action-types.js | 1 + src/redux/actions/workspaces-actions.ts | 9 +- src/redux/reducers/workspaces-reducer.ts | 14 +- .../workspaces/WorkspaceActions.tsx | 168 + .../workspaces/WorkspaceDetail.tsx | 97 + .../workspaces/WorkspaceList.tsx | 29 + ...{workspaces.tsx => WorkspaceListTable.tsx} | 70 +- .../shared/__snapshots__/toolbar.test.js.snap | 1001 ++-- src/test/role/__snapshots__/role.test.js.snap | 4951 +++++------------ .../role/__snapshots__/roles.test.js.snap | 1275 +---- .../inventory-groups-role.test.js.snap | 6 + .../group/__snapshots__/groups.test.js.snap | 171 +- .../add-group-members.test.js.snap | 358 +- .../__snapshots__/group-members.test.js.snap | 1433 +---- .../add-group-service-accounts.test.js.snap | 167 +- .../group-service-accounts.test.js.snap | 182 +- .../CommonBundleView.test.js.snap | 267 +- src/utilities/pathnames.js | 5 + 24 files changed, 3272 insertions(+), 7102 deletions(-) create mode 100644 src/smart-components/workspaces/WorkspaceActions.tsx create mode 100644 src/smart-components/workspaces/WorkspaceDetail.tsx create mode 100644 src/smart-components/workspaces/WorkspaceList.tsx rename src/smart-components/workspaces/{workspaces.tsx => WorkspaceListTable.tsx} (61%) diff --git a/package-lock.json b/package-lock.json index 50e46283d..23115dc80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@data-driven-forms/react-form-renderer": "^3.22.4", "@formatjs/cli": "6.2.2", "@patternfly/quickstarts": "^5.1.0", - "@patternfly/react-component-groups": "^5.4.0-prerelease.2", + "@patternfly/react-component-groups": "^5.5.4", "@patternfly/react-core": "^5.1.1", "@patternfly/react-data-view": "^5.2.0", "@patternfly/react-icons": "^5.1.1", @@ -4030,12 +4030,13 @@ } }, "node_modules/@patternfly/react-component-groups": { - "version": "5.4.0-prerelease.2", - "license": "MIT", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-5.5.4.tgz", + "integrity": "sha512-3qf0CU3vtHGGb38LkfCfZAfrm1zXReFVjpI5E0WryLOpWl+NamuYbKfyY7j1SCj8Zq4kqCTLdXj+je3WBs+GGg==", "dependencies": { - "@patternfly/react-core": "^5.3.3", - "@patternfly/react-icons": "^5.3.2", - "@patternfly/react-table": "^5.3.3", + "@patternfly/react-core": "^5.4.1", + "@patternfly/react-icons": "^5.4.0", + "@patternfly/react-table": "^5.4.1", "clsx": "^2.1.1", "react-jss": "^10.10.0" }, @@ -4052,15 +4053,16 @@ } }, "node_modules/@patternfly/react-core": { - "version": "5.3.4", - "license": "MIT", - "dependencies": { - "@patternfly/react-icons": "^5.3.2", - "@patternfly/react-styles": "^5.3.1", - "@patternfly/react-tokens": "^5.3.1", - "focus-trap": "7.5.2", + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-5.4.8.tgz", + "integrity": "sha512-4KRsQsH39VmTiFPLdN34QqNZg6gKrTamJxKtWEPO1VKA0TpoRMwpFEGk9BDyxipxYST6WzXznAaLCidGkCDlWw==", + "dependencies": { + "@patternfly/react-icons": "^5.4.2", + "@patternfly/react-styles": "^5.4.1", + "@patternfly/react-tokens": "^5.4.1", + "focus-trap": "7.6.0", "react-dropzone": "^14.2.3", - "tslib": "^2.5.0" + "tslib": "^2.7.0" }, "peerDependencies": { "react": "^17 || ^18", @@ -4084,21 +4086,6 @@ "react-dom": "^17 || ^18" } }, - "node_modules/@patternfly/react-data-view/node_modules/@patternfly/react-component-groups": { - "version": "5.3.0-prerelease.2", - "license": "MIT", - "dependencies": { - "@patternfly/react-core": "^5.3.3", - "@patternfly/react-icons": "^5.3.2", - "@patternfly/react-table": "^5.3.3", - "clsx": "^2.1.1", - "react-jss": "^10.10.0" - }, - "peerDependencies": { - "react": "^17 || ^18", - "react-dom": "^17 || ^18" - } - }, "node_modules/@patternfly/react-data-view/node_modules/clsx": { "version": "2.1.1", "license": "MIT", @@ -4107,27 +4094,30 @@ } }, "node_modules/@patternfly/react-icons": { - "version": "5.3.2", - "license": "MIT", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-5.4.2.tgz", + "integrity": "sha512-CMQ5oHYzW6TPVTs2jpNJmP2vGCAKR/YeTPwHGO9dLkAUej1IcIxtCCWK2Fdo2UJsnBjuZihasyw2b6ehvbUm9Q==", "peerDependencies": { "react": "^17 || ^18", "react-dom": "^17 || ^18" } }, "node_modules/@patternfly/react-styles": { - "version": "5.3.1", - "license": "MIT" + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-5.4.1.tgz", + "integrity": "sha512-XA8PXksD8uiA3RTwxdUwJXOCf+V6sVd+2HKapWAdRLvtSV+Sdk7NgCvalb4IAQncsddLopjPQD8gAHA298+N8w==" }, "node_modules/@patternfly/react-table": { - "version": "5.3.4", - "license": "MIT", - "dependencies": { - "@patternfly/react-core": "^5.3.4", - "@patternfly/react-icons": "^5.3.2", - "@patternfly/react-styles": "^5.3.1", - "@patternfly/react-tokens": "^5.3.1", - "lodash": "^4.17.19", - "tslib": "^2.5.0" + "version": "5.4.9", + "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-5.4.9.tgz", + "integrity": "sha512-fSbBZRihVCAaUOKRFzzqYhBrTSI/VGU6O9I0a21T+bXwHz071OsefBdE/ZQiJhqHpJTC+WAZWM76/1CEEnrBFw==", + "dependencies": { + "@patternfly/react-core": "^5.4.8", + "@patternfly/react-icons": "^5.4.2", + "@patternfly/react-styles": "^5.4.1", + "@patternfly/react-tokens": "^5.4.1", + "lodash": "^4.17.21", + "tslib": "^2.7.0" }, "peerDependencies": { "react": "^17 || ^18", @@ -4135,8 +4125,9 @@ } }, "node_modules/@patternfly/react-tokens": { - "version": "5.3.1", - "license": "MIT" + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-5.4.1.tgz", + "integrity": "sha512-eygdHE7Krta1mijAv/E8RHiKIgysD0eeNTo8EXUYC8/M4e5K6sqpr2p6rQBF8QiRMN8FnbXvZT3K2OQ28pYt9Q==" }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -5207,28 +5198,6 @@ "react-router-dom": "^5.0.0 || ^6.0.0" } }, - "node_modules/@redhat-cloud-services/frontend-components/node_modules/@patternfly/react-component-groups": { - "version": "5.2.0", - "license": "MIT", - "dependencies": { - "@patternfly/react-core": "^5.1.1", - "@patternfly/react-icons": "^5.1.1", - "@patternfly/react-table": "^5.1.1", - "clsx": "^2.0.0", - "react-jss": "^10.10.0" - }, - "peerDependencies": { - "react": "^17 || ^18", - "react-dom": "^17 || ^18" - } - }, - "node_modules/@redhat-cloud-services/frontend-components/node_modules/clsx": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/@redhat-cloud-services/host-inventory-client": { "version": "1.5.0", "license": "Apache-2.0", @@ -12255,8 +12224,9 @@ "peer": true }, "node_modules/focus-trap": { - "version": "7.5.2", - "license": "MIT", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.0.tgz", + "integrity": "sha512-1td0l3pMkWJLFipobUcGaf+5DTY4PLDDrcqoSaKP8ediO/CoWCCYk/fT/Y2A4e6TNB+Sh6clRJCjOPPnKoNHnQ==", "dependencies": { "tabbable": "^6.2.0" } @@ -24850,7 +24820,8 @@ }, "node_modules/tabbable": { "version": "6.2.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, "node_modules/tapable": { "version": "2.2.1", @@ -25652,8 +25623,9 @@ "license": "ISC" }, "node_modules/tslib": { - "version": "2.6.2", - "license": "0BSD" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/tunnel-agent": { "version": "0.6.0", diff --git a/package.json b/package.json index 0a4051105..0432e5f77 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@data-driven-forms/react-form-renderer": "^3.22.4", "@formatjs/cli": "6.2.2", "@patternfly/quickstarts": "^5.1.0", - "@patternfly/react-component-groups": "^5.4.0-prerelease.2", + "@patternfly/react-component-groups": "^5.5.4", "@patternfly/react-core": "^5.1.1", "@patternfly/react-data-view": "^5.2.0", "@patternfly/react-icons": "^5.1.1", diff --git a/src/Messages.js b/src/Messages.js index 229171b79..5b9ca86c1 100644 --- a/src/Messages.js +++ b/src/Messages.js @@ -968,6 +968,46 @@ export default defineMessages({ description: 'learn more link', defaultMessage: 'Learn more about workspaces', }, + workspacesActionEditWorkspace: { + id: 'workspacesActionEditWorkspace', + description: 'Menu item Edit workspace', + defaultMessage: 'Edit workspace', + }, + workspacesActionGrantAccessToWorkspace: { + id: 'workspacesActionGrantAccessToWorkspace', + description: 'Menu item Grant access to workspace', + defaultMessage: 'Grant access to workspace', + }, + workspacesActionCreateSubWorkspace: { + id: 'workspacesActionCreateSubWorkspace', + description: 'Menu item Create sub-workspace', + defaultMessage: 'Create sub-workspace', + }, + workspacesActionViewTenant: { + id: 'workspacesActionViewTenant', + description: 'Menu item View tenant', + defaultMessage: 'View tenant', + }, + workspacesActionManageIntegrations: { + id: 'workspacesActionManageIntegrations', + description: 'Menu item Manage integrations', + defaultMessage: 'Manage integrations', + }, + workspacesActionManageNotifications: { + id: 'workspacesActionManageNotifications', + description: 'Menu item Manage notifications', + defaultMessage: 'Manage notifications', + }, + workspacesActionDeleteWorkspace: { + id: 'workspacesActionDeleteWorkspace', + description: 'Menu item Delete workspace', + defaultMessage: 'Delete workspace', + }, + workspacesDetailBreadcrumbTitle: { + id: 'workspacesDetailBreadcrumbTitle', + description: 'Workspace detail breadcrumb title', + defaultMessage: 'Workspace hierarchy: ', + }, viewGroupsBtn: { id: 'viewGroupsBtn', description: 'View groups button', diff --git a/src/Routing.tsx b/src/Routing.tsx index 7fa536c84..4a8c362c5 100644 --- a/src/Routing.tsx +++ b/src/Routing.tsx @@ -11,7 +11,8 @@ import { useFlag } from '@unleash/proxy-client-react'; const Overview = lazy(() => import('./smart-components/overview/overview')); const WorkspacesOverview = lazy(() => import('./smart-components/workspaces/overview/about-access-tab')); -const Workspaces = lazy(() => import('./smart-components/workspaces/workspaces')); +const WorkspaceList = lazy(() => import('./smart-components/workspaces/WorkspaceList')); +const WorkspaceDetail = lazy(() => import('./smart-components/workspaces/WorkspaceDetail')); const Users = lazy(() => import('./smart-components/user/users')); const UserDetail = lazy(() => import('./smart-components/user/user')); const AddUserToGroup = lazy(() => import('./smart-components/user/add-user-to-group/add-user-to-group')); @@ -54,7 +55,11 @@ const getRoutes = ({ enableServiceAccounts, isITLess, isWorkspacesFlag }: Record }, { path: pathnames.workspaces.path, - element: Workspaces, + element: WorkspaceList, + }, + { + path: pathnames['workspace-detail'].path, + element: WorkspaceDetail, }, { path: pathnames['user-detail'].path, diff --git a/src/helpers/workspaces/api.js b/src/helpers/workspaces/api.js index 841d7ee8f..37cb6c63a 100644 --- a/src/helpers/workspaces/api.js +++ b/src/helpers/workspaces/api.js @@ -6,7 +6,6 @@ import listWorkspaces from '@redhat-cloud-services/rbac-client/dist/v2/Workspace import createWorkspace from '@redhat-cloud-services/rbac-client/dist/v2/WorkspacesCreate'; import updateWorkspace from '@redhat-cloud-services/rbac-client/dist/v2/WorkspacesPatch'; import deleteWorkspace from '@redhat-cloud-services/rbac-client/dist/v2/WorkspacesDelete'; -// import listWorkspaces from '@redhat-cloud-services/rbac-client/dist/v2/WorkspacesList'; import { APIFactory } from '@redhat-cloud-services/javascript-clients-shared'; import { RBAC_API_BASE_2 } from '../../utilities/constants'; diff --git a/src/helpers/workspaces/workspaces-helper.ts b/src/helpers/workspaces/workspaces-helper.ts index 8d5003480..f24c9a2ff 100644 --- a/src/helpers/workspaces/workspaces-helper.ts +++ b/src/helpers/workspaces/workspaces-helper.ts @@ -5,3 +5,7 @@ const workspacesApi = getWorkspacesApi(); export async function getWorkspaces() { return await workspacesApi.listWorkspaces(); } + +export async function getWorkspace(ws: string) { + return await workspacesApi.getWorkspace({ id: ws }); +} diff --git a/src/redux/action-types.js b/src/redux/action-types.js index a40b80a6b..75d91e867 100644 --- a/src/redux/action-types.js +++ b/src/redux/action-types.js @@ -69,3 +69,4 @@ export const RESET_EXPAND_SPLATS = 'RESET_EXPAND_SPLATS'; export const API_ERROR = 'API_ERROR'; export const FETCH_WORKSPACES = 'FETCH_WORKSPACES'; +export const FETCH_WORKSPACE = 'FETCH_WORKSPACE'; diff --git a/src/redux/actions/workspaces-actions.ts b/src/redux/actions/workspaces-actions.ts index df930b9ff..f5518265b 100644 --- a/src/redux/actions/workspaces-actions.ts +++ b/src/redux/actions/workspaces-actions.ts @@ -3,7 +3,10 @@ import * as WorkspacesHelper from '../../helpers/workspaces/workspaces-helper'; export const fetchWorkspaces = () => ({ type: ActionTypes.FETCH_WORKSPACES, - payload: WorkspacesHelper.getWorkspaces().catch((err) => { - throw err; - }), + payload: WorkspacesHelper.getWorkspaces(), +}); + +export const fetchWorkspace = (ws: string) => ({ + type: ActionTypes.FETCH_WORKSPACE, + payload: WorkspacesHelper.getWorkspace(ws), }); diff --git a/src/redux/reducers/workspaces-reducer.ts b/src/redux/reducers/workspaces-reducer.ts index 324d5002f..6c0abdd2d 100644 --- a/src/redux/reducers/workspaces-reducer.ts +++ b/src/redux/reducers/workspaces-reducer.ts @@ -1,4 +1,4 @@ -import { FETCH_WORKSPACES } from '../action-types'; +import { FETCH_WORKSPACES, FETCH_WORKSPACE } from '../action-types'; export interface Workspace { id: string; @@ -11,23 +11,35 @@ export interface WorkspacesStore { isLoading: boolean; workspaces: Workspace[]; error: string; + selectedWorkspace: Workspace; } export const workspacesInitialState = { isLoading: false, workspaces: [], error: '', + selectedWorkspace: undefined, }; const setLoadingState = (state: WorkspacesStore) => ({ ...state, isLoading: true }); +const setLoadingDetailState = (state: WorkspacesStore) => ({ ...state, isLoading: true }); + const setWorkspaces = (state: WorkspacesStore, { payload }: { payload: { data: Workspace } }) => ({ ...state, workspaces: payload.data, isLoading: false, }); +const setWorkspace = (state: WorkspacesStore, { payload }: { payload: { data: Workspace } }) => ({ + ...state, + selectedWorkspace: payload.data, + isLoading: false, +}); + export default { [`${FETCH_WORKSPACES}_PENDING`]: setLoadingState, [`${FETCH_WORKSPACES}_FULFILLED`]: setWorkspaces, + [`${FETCH_WORKSPACE}_PENDING`]: setLoadingDetailState, + [`${FETCH_WORKSPACE}_FULFILLED`]: setWorkspace, }; diff --git a/src/smart-components/workspaces/WorkspaceActions.tsx b/src/smart-components/workspaces/WorkspaceActions.tsx new file mode 100644 index 000000000..67c1c8f85 --- /dev/null +++ b/src/smart-components/workspaces/WorkspaceActions.tsx @@ -0,0 +1,168 @@ +import React, { useState } from 'react'; +import { Divider, DrilldownMenu, Menu, MenuContainer, MenuContent, MenuItem, MenuItemAction, MenuList, MenuToggle } from '@patternfly/react-core'; +import { ExternalLinkAltIcon } from '@patternfly/react-icons'; +import { useIntl } from 'react-intl'; +import Messages from '../../Messages'; + +enum ActionType { + EDIT_WORKSPACE = 'EDIT_WORKSPACE', + GRANT_ACCESS = 'GRANT_ACCESS', + CREATE_SUBWORKSPACE = 'CREATE_SUBWORKSPACE', + VIEW_TENANT = 'VIEW_TENANT', + MANAGE_NOTIFICATIONS = 'MANAGE_NOTIFICATIONS', + DELETE_WORKSPACE = 'DELETE_WORKSPACE', + DUMMY_ACTION = 'DUMMY_ACTION', +} + +interface WorkspaceActionsProps { + isDisabled?: boolean; +} + +const WorkspaceActions: React.FC = (props) => { + const [isOpen, setIsOpen] = useState(false); + const menuRef = React.useRef(null); + const toggleRef = React.useRef(null); + const [menuDrilledIn, setMenuDrilledIn] = React.useState([]); + const [drilldownPath, setDrilldownPath] = React.useState([]); + const [menuHeights, setMenuHeights] = React.useState>({}); + const [activeMenu, setActiveMenu] = React.useState('workspaceActions-rootMenu'); + const intl = useIntl(); + + const toggle = ( + setIsOpen(!isOpen)} isExpanded={isOpen} isDisabled={props.isDisabled} variant="default"> + Actions + + ); + + const drillIn = (_event: React.KeyboardEvent | React.MouseEvent, fromMenuId: string, toMenuId: string, pathId: string) => { + setMenuDrilledIn([...menuDrilledIn, fromMenuId]); + setDrilldownPath([...drilldownPath, pathId]); + setActiveMenu(toMenuId); + }; + + const drillOut = (_event: React.KeyboardEvent | React.MouseEvent, toMenuId: string) => { + const menuDrilledInSansLast = menuDrilledIn.slice(0, menuDrilledIn.length - 1); + const pathSansLast = drilldownPath.slice(0, drilldownPath.length - 1); + setMenuDrilledIn(menuDrilledInSansLast); + setDrilldownPath(pathSansLast); + setActiveMenu(toMenuId); + }; + + const setHeight = (menuId: string, height: number) => { + if (menuHeights[menuId] === undefined || (menuId !== 'workspaceActions-rootMenu' && menuHeights[menuId] !== height)) { + setMenuHeights({ ...menuHeights, [menuId]: height }); + } + }; + + const dispatchAction = (action: ActionType) => { + console.log('Dispatched action: ', action); + setIsOpen(!isOpen); + }; + + const menu = ( + + + + dispatchAction(ActionType.EDIT_WORKSPACE)} itemId="edit_workspace"> + {intl.formatMessage(Messages.workspacesActionEditWorkspace)} + + dispatchAction(ActionType.GRANT_ACCESS)} itemId="grant_access"> + {intl.formatMessage(Messages.workspacesActionGrantAccessToWorkspace)} + + dispatchAction(ActionType.CREATE_SUBWORKSPACE)} itemId="create_subworkspace"> + {intl.formatMessage(Messages.workspacesActionCreateSubWorkspace)} + + dispatchAction(ActionType.VIEW_TENANT)} itemId="view_tenant"> + {intl.formatMessage(Messages.workspacesActionViewTenant)} + + + + {intl.formatMessage(Messages.workspacesActionManageIntegrations)} + + + { + dispatchAction(ActionType.DUMMY_ACTION); + }} + itemId="menu_item_1" + > + Menu Item 1 + + { + dispatchAction(ActionType.DUMMY_ACTION); + }} + itemId="menu_item_2" + > + Menu Item 2 + + { + dispatchAction(ActionType.DUMMY_ACTION); + }} + itemId="menu_item_3" + > + Menu Item 3 + + { + dispatchAction(ActionType.DUMMY_ACTION); + }} + itemId="menu_item_4" + > + Menu Item 4 + + + } + > + {intl.formatMessage(Messages.workspacesActionManageIntegrations)} + + dispatchAction(ActionType.MANAGE_NOTIFICATIONS)} + actions={ + } + onClick={() => dispatchAction(ActionType.MANAGE_NOTIFICATIONS)} + aria-label="Manage Notifications" + /> + } + itemId="manage_notifications" + > + {intl.formatMessage(Messages.workspacesActionManageNotifications)} + + dispatchAction(ActionType.DELETE_WORKSPACE)} itemId="delete_workspace"> + {intl.formatMessage(Messages.workspacesActionDeleteWorkspace)} + + + + + ); + + return ( + setIsOpen(isOpen)} + menu={menu} + menuRef={menuRef} + toggle={toggle} + toggleRef={toggleRef} + popperProps={{ position: 'end' }} + /> + ); +}; + +export default WorkspaceActions; diff --git a/src/smart-components/workspaces/WorkspaceDetail.tsx b/src/smart-components/workspaces/WorkspaceDetail.tsx new file mode 100644 index 000000000..1b8c088d9 --- /dev/null +++ b/src/smart-components/workspaces/WorkspaceDetail.tsx @@ -0,0 +1,97 @@ +import React, { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { useDispatch, useSelector } from 'react-redux'; +import { fetchWorkspace, fetchWorkspaces } from '../../redux/actions/workspaces-actions'; +import { ContentHeader } from '@patternfly/react-component-groups'; +import { Breadcrumb, BreadcrumbItem, Divider, PageSection, Text, TextContent, TextVariants } from '@patternfly/react-core'; +import { RBACStore } from '../../redux/store'; +import { useParams } from 'react-router-dom'; +import AppTabs from '../app-tabs/app-tabs'; +import pathnames from '../../utilities/pathnames'; +import WorkspaceActions from './WorkspaceActions'; +import Messages from '../../Messages'; + +interface WorkspaceData { + name: string; + id: string; +} + +const WorkspaceDetail = () => { + const intl = useIntl(); + const { workspaceId } = useParams(); + + const dispatch = useDispatch(); + + const { isLoading, workspaces, selectedWorkspace, error } = useSelector((state: RBACStore) => state.workspacesReducer); + const [workspaceHierarchy, setWorkspaceHierarchy] = useState([]); + + useEffect(() => { + if (workspaceId && workspaceId.length > 0) { + dispatch(fetchWorkspace(workspaceId)); + } + dispatch(fetchWorkspaces()); + }, [dispatch]); + + const buildWorkspacesHierarchy = (workspaces: any[], workspaceId: string): WorkspaceData[] => { + let currentWorkspace = workspaces.find((ws) => ws.id === workspaceId); + + const hierarchy: WorkspaceData[] = currentWorkspace ? [currentWorkspace] : []; + while (currentWorkspace?.parent_id?.length > 0) { + currentWorkspace = workspaces.find((ws) => ws.id === currentWorkspace?.parent_id); + if (!currentWorkspace) break; + hierarchy.unshift({ name: currentWorkspace.name, id: currentWorkspace.id }); + } + + return hierarchy; + }; + + useEffect(() => { + if (workspaces.length > 0 && workspaceId) { + setWorkspaceHierarchy(buildWorkspacesHierarchy(workspaces, workspaceId)); + } + }, [workspaces]); + + const tabItems = [ + { eventKey: 0, title: 'Role assignments', name: pathnames['workspace-detail'].link, to: '' }, + { eventKey: 1, title: 'Assets', name: pathnames['workspace-detail'].link, to: '' }, + { eventKey: 2, title: 'Features management', name: pathnames['workspace-detail'].link, to: '' }, + ]; + + return ( + + } + > +
+ {intl.formatMessage(Messages.workspacesDetailBreadcrumbTitle)} + + {workspaceHierarchy.map((workspace, index) => ( + + {workspace.name} + + ))} + +
+
+ + + + {isLoading &&

Loading state...

} + {error &&

Error state: {error}

} + {!isLoading && !error && ( + + Tab content to be placed here + + )} +
+
+ ); +}; + +export default WorkspaceDetail; diff --git a/src/smart-components/workspaces/WorkspaceList.tsx b/src/smart-components/workspaces/WorkspaceList.tsx new file mode 100644 index 000000000..7923fc940 --- /dev/null +++ b/src/smart-components/workspaces/WorkspaceList.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { useIntl } from 'react-intl'; +import messages from '../../Messages'; +import { ContentHeader } from '@patternfly/react-component-groups'; +import { PageSection } from '@patternfly/react-core'; +import WorkspaceListTable from './WorkspaceListTable'; + +const WorkspaceList = () => { + const intl = useIntl(); + + return ( + + + + + + + ); +}; + +export default WorkspaceList; diff --git a/src/smart-components/workspaces/workspaces.tsx b/src/smart-components/workspaces/WorkspaceListTable.tsx similarity index 61% rename from src/smart-components/workspaces/workspaces.tsx rename to src/smart-components/workspaces/WorkspaceListTable.tsx index a8556cad5..978fbe6e7 100644 --- a/src/smart-components/workspaces/workspaces.tsx +++ b/src/smart-components/workspaces/WorkspaceListTable.tsx @@ -1,16 +1,14 @@ import React, { useEffect, useState } from 'react'; -import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { fetchWorkspaces } from '../../redux/actions/workspaces-actions'; -import messages from '../../Messages'; -import { BulkSelect, BulkSelectValue, ContentHeader } from '@patternfly/react-component-groups'; -import { PageSection } from '@patternfly/react-core'; +import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups'; import { DataView, DataViewTable, DataViewTh, DataViewToolbar, DataViewTrTree, useDataViewSelection } from '@patternfly/react-data-view'; import { Workspace } from '../../redux/reducers/workspaces-reducer'; import { RBACStore } from '../../redux/store'; +import AppLink from '../../presentational-components/shared/AppLink'; +import pathnames from '../../utilities/pathnames'; -const Workspaces = () => { - const intl = useIntl(); +const WorkspaceListTable = () => { const dispatch = useDispatch(); const selection = useDataViewSelection({ matchOption: (a, b) => a.id === b.id }); @@ -54,7 +52,16 @@ const Workspaces = () => { const buildRows = (workspaces: Workspace[]): DataViewTrTree[] => workspaces.map((workspace) => ({ - row: [workspace.name, workspace.description], + row: [ + + {workspace.name} + , + workspace.description, + ], id: workspace.id, ...(workspace.children && workspace.children.length > 0 ? { @@ -74,37 +81,26 @@ const Workspaces = () => { return ( - - - {isLoading &&

Loading...

} - {error &&

Error: {error}

} - {!isLoading && !error && ( - - - } - /> - - - )} -
+ {isLoading &&

Loading state...

} + {error &&

Error state: {error}

} + {!isLoading && !error && ( + + + } + /> + + + )}
); }; -export default Workspaces; +export default WorkspaceListTable; diff --git a/src/test/presentional-components/shared/__snapshots__/toolbar.test.js.snap b/src/test/presentional-components/shared/__snapshots__/toolbar.test.js.snap index 9396dff12..13f5bd94e 100644 --- a/src/test/presentional-components/shared/__snapshots__/toolbar.test.js.snap +++ b/src/test/presentional-components/shared/__snapshots__/toolbar.test.js.snap @@ -24,7 +24,9 @@ exports[` checkedRows is loading - false 1`] = ` - + +