diff --git a/jestSetupAfterEnv.tsx b/jestSetupAfterEnv.tsx index dd45486f7..2219a795d 100644 --- a/jestSetupAfterEnv.tsx +++ b/jestSetupAfterEnv.tsx @@ -169,3 +169,10 @@ jest.mock('./src/react/next-architecture/ui/XCoreLibraryProvider', () => { }); jest.mock('@module-federation/enhanced/runtime', () => {}, { virtual: true }); + +jest.mock('@scality/module-federation', () => ({ + useCurrentApp: () => ({ + name: 'zenko-ui', + appHistoryBasePath: '', + }), +})); diff --git a/src/QueryClientProvider.tsx b/src/QueryClientProvider.tsx new file mode 100644 index 000000000..915ba3a36 --- /dev/null +++ b/src/QueryClientProvider.tsx @@ -0,0 +1,11 @@ +import { + QueryClient, + QueryClientProvider as BaseQueryClientProvider, +} from 'react-query'; + +export const QueryClientProvider = + BaseQueryClientProvider as React.ComponentType<{ + client: QueryClient; + contextSharing?: boolean; + children?: React.ReactNode; + }>; diff --git a/src/react/DataServiceRoleProvider.tsx b/src/react/DataServiceRoleProvider.tsx index 5421234fb..7fe601e46 100644 --- a/src/react/DataServiceRoleProvider.tsx +++ b/src/react/DataServiceRoleProvider.tsx @@ -86,7 +86,7 @@ export const useCurrentAccount = () => { else if (accountId) return account.id === accountId; else return true; }); - }, [accountId, JSON.stringify(accounts)]); + }, [accountId, JSON.stringify(accounts), accountName]); return { account, @@ -153,7 +153,7 @@ const DataServiceRoleProvider = ({ if (role.roleArn) { assumeRoleMutation.mutate(role.roleArn); } - }, [role.roleArn, JSON.stringify(accounts), userData?.token]); + }, [role.roleArn, JSON.stringify(accounts), userData?.token, accountName]); const { getS3Config } = useS3ConfigFromAssumeRoleResult(); diff --git a/src/react/Routes.tsx b/src/react/Routes.tsx index 4f535a4b8..e02ba7972 100644 --- a/src/react/Routes.tsx +++ b/src/react/Routes.tsx @@ -45,6 +45,7 @@ import LocationEditor from './locations/LocationEditor'; import { useBasenameRelativeNavigate } from './ShellHooksContext'; import Workflows from './workflow/Workflows'; import CreateWorkflow from './workflow/CreateWorkflow'; +import Objects from './databrowser/objects/Objects'; export const RemoveTrailingSlash = ({ ...rest }) => { const location = useLocation(); @@ -186,6 +187,10 @@ function PrivateRoutes() { path="accounts/:accountName/create-bucket/*" element={} /> + } + /> } @@ -214,13 +219,17 @@ function InternalRoutes() { const doesRouteMatch = useCallback( (paths: string | string[]) => { if (Array.isArray(paths)) { - const foundMatchingRoute = paths.find( - (path) => - !!matchPath(config.basePath + path + '/*', location.pathname), + return paths.some((path) => + matchPath( + { path: config.basePath + path, end: false }, + location.pathname, + ), ); - return !!foundMatchingRoute; } else { - return !!matchPath(config.basePath + paths + '/*', location.pathname); + return !!matchPath( + { path: config.basePath + paths, end: false }, + location.pathname, + ); } }, [location.pathname], @@ -270,7 +279,8 @@ function InternalRoutes() { }, active: doesRouteMatch('/buckets') || - doesRouteMatch('/accounts/:accountName/buckets'), + doesRouteMatch('/accounts/:accountName/buckets') || + doesRouteMatch('/accounts/:accountName/data/buckets'), }, { label: 'Workflows', @@ -280,7 +290,8 @@ function InternalRoutes() { }, active: doesRouteMatch('/workflows') || - doesRouteMatch('/accounts/:accountName/workflows'), + doesRouteMatch('/accounts/:accountName/workflows') || + doesRouteMatch('/accounts/:accountName/data/workflows'), }, ...(isStorageManager ? [ diff --git a/src/react/account/AccountRoleSelectButtonAndModal.tsx b/src/react/account/AccountRoleSelectButtonAndModal.tsx index a23d4b3a4..bd8f1348f 100644 --- a/src/react/account/AccountRoleSelectButtonAndModal.tsx +++ b/src/react/account/AccountRoleSelectButtonAndModal.tsx @@ -1,6 +1,7 @@ import { Icon, Stack, Tooltip, Wrap } from '@scality/core-ui'; import { Box, Button, Table } from '@scality/core-ui/dist/next'; import { useMemo, useState } from 'react'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useCurrentAccount, useDataServiceRole, @@ -132,6 +133,8 @@ export function AccountRoleSelectButtonAndModal({ setIsModalOpen(false); }; + console.log({ accountName }); + return ( <> { - const navigate = useBasenameRelativeNavigate(); + const navigateWithBasename = useBasenameRelativeNavigate(); + const navigate = useNavigate(); + const { accountName } = useParams(); + const location = useLocation(); + + const handleAccountClick = () => { + const replacePath = location.pathname.replace(accountName, assumedAccount); + + if (replacePath.includes('/buckets')) { + navigateWithBasename(`/accounts/${assumedAccount}/data/buckets`); + } else if (replacePath.includes('/workflows')) { + navigateWithBasename(`/accounts/${assumedAccount}/data/workflows`); + } else { + navigate(replacePath); + } + }; return ( @@ -201,7 +219,7 @@ const ModalFooter = ({ variant="primary" onClick={() => { setRole({ roleArn: assumedRoleArn }); - navigate(`/accounts/${assumedAccount}/buckets`); + handleAccountClick(); handleClose(); }} label="Continue" diff --git a/src/react/databrowser/buckets/Buckets.tsx b/src/react/databrowser/buckets/Buckets.tsx index 640e4bb58..080c2b7d7 100644 --- a/src/react/databrowser/buckets/Buckets.tsx +++ b/src/react/databrowser/buckets/Buckets.tsx @@ -28,7 +28,7 @@ export default function Buckets() { (state: AppState) => state.instanceStatus.latest.metrics?.['ingest-schedule']?.states, ); - const { bucketName: bucketNameParam } = useParams<{ + const { bucketName: bucketNameParam, accountName } = useParams<{ bucketName: string; accountName: string; }>(); @@ -85,9 +85,11 @@ export default function Buckets() { } // Replace the old bucket name with the new one when switching accounts + if ( bucketNameParam && - !buckets.value.some((bucket) => bucket.name === bucketNameParam) + !buckets.value.some((bucket) => bucket.name === bucketNameParam) && + accountName === account?.Name ) { return ( { const [hintsShown, setHintsShown] = useState(false); - const navigate = useBasenameRelativeNavigate(); + const navigate = useNavigate(); const query = useQueryParams(); const { pathname } = useLocation(); const prefixWithSlash = usePrefixWithSlash(); diff --git a/src/react/databrowser/objects/ObjectList.tsx b/src/react/databrowser/objects/ObjectList.tsx index 1919d2770..ebd481ca4 100644 --- a/src/react/databrowser/objects/ObjectList.tsx +++ b/src/react/databrowser/objects/ObjectList.tsx @@ -1,25 +1,24 @@ -import * as T from '../../ui-elements/Table'; +import { Icon, spacing, Toggle } from '@scality/core-ui'; +import { Box } from '@scality/core-ui/dist/next'; +import { List } from 'immutable'; +import { useDispatch, useSelector } from 'react-redux'; +import { useLocation, useNavigate } from 'react-router-dom'; import { ListObjectsType, ObjectEntity } from '../../../types/s3'; -import { LIST_OBJECT_VERSIONS_S3_TYPE } from '../../utils/s3'; -import { maybePluralize } from '../../utils'; +import { AppState } from '../../../types/state'; import { openFolderCreateModal, openObjectDeleteModal, openObjectUploadModal, } from '../../actions'; -import { useDispatch, useSelector } from 'react-redux'; -import { AppState } from '../../../types/state'; -import { List } from 'immutable'; -import MetadataSearch from './MetadataSearch'; -import ObjectListTable from './ObjectListTable'; -import { Icon, spacing, Toggle } from '@scality/core-ui'; -import { WarningMetadata } from '../../ui-elements/Warning'; -import { useQueryParams } from '../../utils/hooks'; -import { useLocation } from 'react-router-dom'; -import { Box } from '@scality/core-ui/dist/next'; import { useBucketVersionning } from '../../next-architecture/domain/business/buckets'; +import * as T from '../../ui-elements/Table'; import { VEEAM_XML_PREFIX } from '../../ui-elements/Veeam/VeeamConstants'; -import { useBasenameRelativeNavigate } from '../../ShellHooksContext'; +import { WarningMetadata } from '../../ui-elements/Warning'; +import { maybePluralize } from '../../utils'; +import { useQueryParams } from '../../utils/hooks'; +import { LIST_OBJECT_VERSIONS_S3_TYPE } from '../../utils/s3'; +import MetadataSearch from './MetadataSearch'; +import ObjectListTable from './ObjectListTable'; type Props = { objects: List; bucketName: string; @@ -35,7 +34,7 @@ export default function ObjectList({ toggled, listType, }: Props) { - const navigate = useBasenameRelativeNavigate(); + const navigate = useNavigate(); const dispatch = useDispatch(); const { pathname } = useLocation(); const query = useQueryParams(); diff --git a/src/react/databrowser/objects/ObjectRow.tsx b/src/react/databrowser/objects/ObjectRow.tsx index 249aa13f3..95ab5a0e9 100644 --- a/src/react/databrowser/objects/ObjectRow.tsx +++ b/src/react/databrowser/objects/ObjectRow.tsx @@ -1,7 +1,7 @@ import isDeepEqual from 'lodash.isequal'; import memoize from 'memoize-one'; import { memo } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { areEqual } from 'react-window'; import { Dispatch } from 'redux'; @@ -10,7 +10,6 @@ import { ObjectEntity } from '../../../types/s3'; import { toggleAllObjects } from '../../actions'; import * as T from '../../ui-elements/Table'; import { useQueryParams } from '../../utils/hooks'; -import { useBasenameRelativeNavigate } from '../../ShellHooksContext'; type PrepareRow = (arg0: RowType) => void; type RowType = { @@ -50,7 +49,7 @@ const Row = ({ }: RowProps) => { const row = rows[index]; prepareRow(row); - const navigate = useBasenameRelativeNavigate(); + const navigate = useNavigate(); const { pathname } = useLocation(); const query = useQueryParams(); const versionId = query.get('versionId'); diff --git a/src/react/databrowser/objects/Objects.tsx b/src/react/databrowser/objects/Objects.tsx index dece330c1..411ecc069 100644 --- a/src/react/databrowser/objects/Objects.tsx +++ b/src/react/databrowser/objects/Objects.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; -import { Redirect, useParams } from 'react-router-dom'; +import { Navigate, useParams } from 'react-router-dom'; import { getObjectMetadata, listObjects, @@ -117,7 +117,7 @@ export default function Objects() { } if (!bucketNameParam) { - return ; + return ; } // TODO: manage empty state diff --git a/src/react/ui-elements/Breadcrumb.tsx b/src/react/ui-elements/Breadcrumb.tsx index 304d9c0d0..80a1a9c3e 100644 --- a/src/react/ui-elements/Breadcrumb.tsx +++ b/src/react/ui-elements/Breadcrumb.tsx @@ -6,6 +6,7 @@ import { import styled from 'styled-components'; import AccountRoleSelectButtonAndModal from '../account/AccountRoleSelectButtonAndModal'; import { fontSize } from '@scality/core-ui/dist/style/theme'; +import { useConfig } from '../next-architecture/ui/ConfigProvider'; // vendor from `polished` package type Styles = { @@ -224,8 +225,10 @@ export function Breadcrumb({ breadcrumbPaths }: Props) { } export function BreadcrumbAccount({ pathname }: { pathname: string }) { + const config = useConfig(); + const matchAccountUserAccessKey = matchPath( - '/accounts/:accountName/users/:userName/access-keys', + config.basePath + '/accounts/:accountName/users/:userName/access-keys', pathname, ); @@ -241,7 +244,10 @@ export function BreadcrumbAccount({ pathname }: { pathname: string }) { ); } - const matchAccountRoute = matchPath('/accounts/:accountName', pathname); + const matchAccountRoute = matchPath( + config.basePath + '/accounts/:accountName' + '/*', + pathname, + ); if (matchAccountRoute) { return ( @@ -251,7 +257,10 @@ export function BreadcrumbAccount({ pathname }: { pathname: string }) { ); } - const matchAllAccountsRoute = matchPath('/accounts/', pathname); + const matchAllAccountsRoute = matchPath( + config.basePath + '/accounts/', + pathname, + ); if (matchAllAccountsRoute) { return ( diff --git a/src/react/ui-elements/PrivateRoute.tsx b/src/react/ui-elements/PrivateRoute.tsx index 16e95a60a..17d69cc0f 100644 --- a/src/react/ui-elements/PrivateRoute.tsx +++ b/src/react/ui-elements/PrivateRoute.tsx @@ -1,4 +1,4 @@ -import { Redirect, Route } from 'react-router-dom'; +import { Navigate, Route } from 'react-router-dom'; import React from 'react'; import { connect } from 'react-redux'; @@ -8,16 +8,7 @@ function PrivateRoute(props) { if (props.authenticated) { return ; } else { - return ( - - ); + return ; } } @@ -28,4 +19,4 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(PrivateRoute); \ No newline at end of file +export default connect(mapStateToProps)(PrivateRoute); diff --git a/src/react/utils/testUtil.tsx b/src/react/utils/testUtil.tsx index 5d7d8c301..7602118bc 100644 --- a/src/react/utils/testUtil.tsx +++ b/src/react/utils/testUtil.tsx @@ -1,14 +1,8 @@ import { ReactWrapper, mount } from 'enzyme'; -import { createMemoryHistory } from 'history'; import { PropsWithChildren, ReactNode } from 'react'; -import { - QueryClient, - QueryClientProvider, - setLogger, - useMutation, -} from 'react-query'; +import { QueryClient, setLogger, useMutation } from 'react-query'; import { Provider } from 'react-redux'; -import { Route, Router } from 'react-router-dom'; +import { MemoryRouter, Route } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { ThemeProvider } from 'styled-components'; @@ -20,6 +14,7 @@ import { coreUIAvailableThemes } from '@scality/core-ui/dist/style/theme'; import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { applyMiddleware, compose, createStore } from 'redux'; +import { QueryClientProvider } from '../../QueryClientProvider'; import ZenkoClient from '../../js/ZenkoClient'; import { VEEAM_FEATURE, XDM_FEATURE } from '../../js/config'; import { UiFacingApiWrapper } from '../../js/managementClient'; @@ -43,7 +38,6 @@ import ErrorHandlerModal from '../ui-elements/ErrorHandlerModal'; import ReauthDialog from '../ui-elements/ReauthDialog'; export const theme = coreUIAvailableThemes.darkRebrand; -export const navigate = createMemoryHistory(); export const configuration = { latest: { version: 1, @@ -193,7 +187,7 @@ export const Wrapper = ({ children }: { children: ReactNode }) => { - + <_ConfigContext.Provider //@ts-expect-error fix this when you are working on it value={zenkoUITestConfig} @@ -231,7 +225,7 @@ export const Wrapper = ({ children }: { children: ReactNode }) => { - + @@ -423,7 +417,6 @@ export function renderWithRouterMatch( { path = '/', route = '/' } = {}, testState?: unknown, ) { - const navigate = createMemoryHistory({ initialEntries: [route] }); const store = realStoreWithInitState(testState); const role = { roleArn: TEST_ROLE_ARN, @@ -435,7 +428,7 @@ export function renderWithRouterMatch( - + <_DataServiceRoleContext.Provider //@ts-expect-error fix this when you are working on it @@ -473,7 +466,7 @@ export function renderWithRouterMatch( - + @@ -487,7 +480,6 @@ export const renderWithCustomRoute = ( route: string, testState?: unknown, ) => { - const navigate = createMemoryHistory({ initialEntries: [route] }); const store = realStoreWithInitState(testState); const role = { roleArn: TEST_ROLE_ARN, @@ -498,7 +490,7 @@ export const renderWithCustomRoute = ( - + <_ConfigContext.Provider //@ts-expect-error fix this when you are working on it value={zenkoUITestConfig} @@ -539,7 +531,7 @@ export const renderWithCustomRoute = ( - + , @@ -572,17 +564,14 @@ const DataServiceProvider = ({ children }) => { export const NewWrapper = (route = '/', testState: unknown = {}) => ({ children }: { children: ReactNode }) => { - const navigate = createMemoryHistory({ initialEntries: [route] }); const store = realStoreWithInitState(testState); - - // const navigate = createMemoryHistory(); // const store = realStoreWithInitState({}); return ( - + <_ManagementContext.Provider @@ -617,7 +606,7 @@ export const NewWrapper = - +