diff --git a/frontend/packages/console-app/src/components/detect-cluster/cluster.ts b/frontend/packages/console-app/src/components/detect-cluster/cluster.ts index 76cd7ec96f9..3e4e1c85a88 100644 --- a/frontend/packages/console-app/src/components/detect-cluster/cluster.ts +++ b/frontend/packages/console-app/src/components/detect-cluster/cluster.ts @@ -2,10 +2,18 @@ import * as React from 'react'; // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore: FIXME missing exports due to out-of-sync @types/react-redux version import { useDispatch } from 'react-redux'; -import { setActiveCluster } from '@console/internal/actions/ui'; +import { useLocation } from 'react-router-dom'; +import isMultiClusterEnabled from '@console/app/src/utils/isMultiClusterEnabled'; +import { setActiveCluster, formatNamespaceRoute } from '@console/internal/actions/ui'; +import { getCluster } from '@console/internal/components/utils/link'; +import { history } from '@console/internal/components/utils/router'; +// import { useActiveNamespace } from '@console/shared'; +import store from '@console/internal/redux'; import { LAST_CLUSTER_USER_SETTINGS_KEY } from '@console/shared/src/constants'; import { useUserSettings } from '@console/shared/src/hooks/useUserSettings'; +export const multiClusterRoutePrefixes = ['/k8s/all-namespaces', '/k8s/cluster', '/k8s/ns']; + type ClusterContextType = { cluster?: string; setCluster?: (cluster: string) => void; @@ -28,14 +36,41 @@ export const useValuesForClusterContext = () => { [dispatch, setLastCluster], ); + // const [activeNamespace] = useActiveNamespace(); + // const activeNamespace = useSelector(({ UI }) => UI.get('activeNamespace')); + + const urlCluster = getCluster(useLocation().pathname); React.useEffect(() => { - // TODO: Detect cluster from URL. - if (lastClusterLoaded && lastCluster) { + if (urlCluster) { + setLastCluster(urlCluster); + dispatch(setActiveCluster(urlCluster)); + } else if (lastClusterLoaded && lastCluster) { dispatch(setActiveCluster(lastCluster)); } - // Only run this hook after last cluster is loaded. + + if ( + isMultiClusterEnabled() && + lastClusterLoaded && + lastCluster && + !urlCluster && + multiClusterRoutePrefixes.some((pattern) => window.location.pathname.startsWith(pattern)) + ) { + const activeNamespace = store.getState().UI.get('activeNamespace'); + const newPath = formatNamespaceRoute( + activeNamespace, + window.location.pathname, + window.location, + false, + lastCluster, + ); + + if (newPath !== window.location.pathname) { + history.pushPath(newPath); + } + } + // Only run this hook after last cluster is loaded or window path changes. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [lastClusterLoaded]); + }, [lastClusterLoaded, urlCluster, window.location.pathname]); return { cluster: lastCluster, diff --git a/frontend/packages/console-shared/src/utils/__tests__/user-settings.spec.ts b/frontend/packages/console-shared/src/utils/__tests__/user-settings.spec.ts index 91d1bb2e94d..28e2ee8b68f 100644 --- a/frontend/packages/console-shared/src/utils/__tests__/user-settings.spec.ts +++ b/frontend/packages/console-shared/src/utils/__tests__/user-settings.spec.ts @@ -40,7 +40,10 @@ describe('createConfigMap', () => { expect(actual).toEqual(configMap); expect(coFetchMock).toHaveBeenCalledTimes(1); - expect(coFetchMock).lastCalledWith('/api/console/user-settings', { method: 'POST' }); + expect(coFetchMock).lastCalledWith('/api/console/user-settings', { + headers: { 'X-Cluster': 'local-cluster' }, + method: 'POST', + }); }); }); @@ -61,6 +64,7 @@ describe('updateConfigMap', () => { headers: { Accept: 'application/json', 'Content-Type': 'application/merge-patch+json;charset=UTF-8', + 'X-Cluster': 'local-cluster', }, body: '{"data":{"key":"value"}}', }, diff --git a/frontend/public/actions/ui.ts b/frontend/public/actions/ui.ts index 9b4d351212e..a373b898c07 100644 --- a/frontend/public/actions/ui.ts +++ b/frontend/public/actions/ui.ts @@ -10,6 +10,7 @@ import { ALL_NAMESPACES_KEY, LAST_NAMESPACE_NAME_LOCAL_STORAGE_KEY, } from '@console/shared/src/constants'; +// import { multiClusterRoutePrefixes } from '@console/app/src/components/detect-cluster/cluster' import { K8sResourceKind, PodKind, NodeKind } from '../module/k8s'; import { allModels } from '../module/k8s/k8s-models'; import { detectFeatures, clearSSARFlags } from './features'; @@ -152,10 +153,23 @@ export const formatNamespaceRoute = ( originalPath, location?, forceList?: boolean, + activeCluster?: string, ) => { let path = originalPath.substr(window.SERVER_FLAGS.basePath.length); let parts = path.split('/').filter((p) => p); + + if (parts[0] === 'cluster') { + // The url pattern that includes the cluster name starts with /cluster/:clusterName + parts.splice(0, 2); + } + + const multiClusterRoutePrefixes = ['/k8s/all-namespaces', '/k8s/cluster', '/k8s/ns']; + const clusterPathPart = + activeCluster && multiClusterRoutePrefixes.includes(`/${parts[0]}/${parts[1]}`) + ? `/cluster/${activeCluster}` + : ''; + const prefix = parts.shift(); let previousNS; @@ -168,7 +182,7 @@ export const formatNamespaceRoute = ( } if (!previousNS) { - return originalPath; + return `${clusterPathPart}${originalPath}`; } if ( @@ -193,7 +207,7 @@ export const formatNamespaceRoute = ( path += `${location.search}${location.hash}`; } - return path; + return `${clusterPathPart}${path}`; }; export const setCurrentLocation = (location: string) => @@ -211,6 +225,7 @@ export const setActiveNamespace = (namespace: string = '') => { // make it noop when new active namespace is the same // otherwise users will get page refresh and cry about // broken direct links and bookmarks + if (namespace !== getActiveNamespace()) { const oldPath = window.location.pathname; const newPath = formatNamespaceRoute(namespace, oldPath, window.location); diff --git a/frontend/public/components/app-contents.tsx b/frontend/public/components/app-contents.tsx index a0c94f2948e..7e06063750d 100644 --- a/frontend/public/components/app-contents.tsx +++ b/frontend/public/components/app-contents.tsx @@ -291,6 +291,17 @@ const AppContents: React.FC<{}> = () => { /> )} /> + ( + + )} + /> = () => { ) } /> + + import('./events' /* webpackChunkName: "events" */).then((m) => + NamespaceFromURL(m.EventStreamPage), + ) + } + /> + = () => { ) } /> + + import('./events' /* webpackChunkName: "events" */).then((m) => + NamespaceFromURL(m.EventStreamPage), + ) + } + /> + @@ -323,6 +354,16 @@ const AppContents: React.FC<{}> = () => { ) } /> + + import('./import-yaml' /* webpackChunkName: "import-yaml" */).then((m) => + NamespaceFromURL(m.ImportYamlPage), + ) + } + /> + = () => { ) } /> + + import('./import-yaml' /* webpackChunkName: "import-yaml" */).then((m) => + NamespaceFromURL(m.ImportYamlPage), + ) + } + /> { // These pages are temporarily disabled. We need to update the safe resources list. @@ -354,6 +404,17 @@ const AppContents: React.FC<{}> = () => { ) } /> + + import('./secrets/create-secret' /* webpackChunkName: "create-secret" */).then( + (m) => m.CreateSecret, + ) + } + /> + = () => { ) } /> + + import('./secrets/create-secret' /* webpackChunkName: "create-secret" */).then( + (m) => m.EditSecret, + ) + } + /> + import('./create-yaml').then((m) => m.EditYAMLPage)} /> + import('./create-yaml').then((m) => m.EditYAMLPage)} + /> = () => { ).then((m) => m.CreateNetworkPolicy) } /> + + import( + '@console/app/src/components/network-policies/create-network-policy' /* webpackChunkName: "create-network-policy" */ + ).then((m) => m.CreateNetworkPolicy) + } + /> = () => { ) } /> + + import('./routes/create-route' /* webpackChunkName: "create-route" */).then( + (m) => m.CreateRoute, + ) + } + /> = () => { } kind="RoleBinding" /> + + import('./RBAC' /* webpackChunkName: "rbac" */).then((m) => m.CreateRoleBinding) + } + kind="RoleBinding" + /> + = () => { } kind="RoleBinding" /> + + import('./RBAC' /* webpackChunkName: "rbac" */).then((m) => m.CreateRoleBinding) + } + kind="RoleBinding" + /> + = () => { import('./RBAC' /* webpackChunkName: "rbac" */).then((m) => m.CopyRoleBinding) } /> + + import('./RBAC' /* webpackChunkName: "rbac" */).then((m) => m.CopyRoleBinding) + } + /> + = () => { import('./RBAC' /* webpackChunkName: "rbac" */).then((m) => m.EditRoleBinding) } /> + + import('./RBAC' /* webpackChunkName: "rbac" */).then((m) => m.EditRoleBinding) + } + /> + = () => { import('./RBAC' /* webpackChunkName: "rbac" */).then((m) => m.CopyRoleBinding) } /> + + import('./RBAC' /* webpackChunkName: "rbac" */).then((m) => m.CopyRoleBinding) + } + /> + = () => { import('./RBAC' /* webpackChunkName: "rbac" */).then((m) => m.EditRoleBinding) } /> + + import('./RBAC' /* webpackChunkName: "rbac" */).then((m) => m.EditRoleBinding) + } + /> + = () => { ) } /> + + import('./storage/attach-storage' /* webpackChunkName: "attach-storage" */).then( + (m) => m.default, + ) + } + /> = () => { ) } /> + + import('./storage/create-pvc' /* webpackChunkName: "create-pvc" */).then( + (m) => m.CreatePVC, + ) + } + /> = () => { ).then((m) => m.VolumeSnapshot) } /> + + import( + '@console/app/src/components/volume-snapshot/create-volume-snapshot/create-volume-snapshot' /* webpackChunkName: "create-volume-snapshot" */ + ).then((m) => m.VolumeSnapshot) + } + /> = () => { ).then((m) => m.VolumeSnapshot) } /> + + import( + '@console/app/src/components/volume-snapshot/create-volume-snapshot/create-volume-snapshot' /* webpackChunkName: "create-volume-snapshot" */ + ).then((m) => m.VolumeSnapshot) + } + /> = () => { ) } /> + + import('./storage-class-form' /* webpackChunkName: "storage-class-form" */).then( + (m) => m.StorageClassForm, + ) + } + /> + {/* START of new links */} + + + + + + import('./container').then((m) => m.ContainersDetailsPage)} /> + import('./container').then((m) => m.ContainersDetailsPage)} + /> + + + + + + + + + + {/* END of new links */} {inactivePluginPageRoutes} diff --git a/frontend/public/components/app.jsx b/frontend/public/components/app.jsx index 3a061b75c9b..7b06490cfce 100644 --- a/frontend/public/components/app.jsx +++ b/frontend/public/components/app.jsx @@ -204,9 +204,9 @@ class App_ extends React.PureComponent { ); return ( - - - + + + {contextProviderExtensions.reduce( (children, e) => ( @@ -215,9 +215,9 @@ class App_ extends React.PureComponent { ), content, )} - - - + + + ); } } diff --git a/frontend/public/components/nav/nav-header.tsx b/frontend/public/components/nav/nav-header.tsx index 2b0ca6ce513..e349675b49c 100644 --- a/frontend/public/components/nav/nav-header.tsx +++ b/frontend/public/components/nav/nav-header.tsx @@ -58,7 +58,7 @@ const NavHeader: React.FC = ({ onPerspectiveSelected }) => { dispatch(clearSSARFlags()); dispatch(detectFeatures()); const oldPath = window.location.pathname; - const newPath = formatNamespaceRoute(activeNamespace, oldPath, window.location, true); + const newPath = formatNamespaceRoute(activeNamespace, oldPath, window.location, true, cluster); if (newPath !== oldPath) { history.pushPath(newPath); } diff --git a/frontend/public/components/utils/link.tsx b/frontend/public/components/utils/link.tsx index ed82e6bd2a5..89498b3fa25 100644 --- a/frontend/public/components/utils/link.tsx +++ b/frontend/public/components/utils/link.tsx @@ -29,10 +29,18 @@ export const namespacedPrefixes = [ '/provisionedservices', '/search', '/status', + '/cluster/:cluster/k8s', ]; export const stripBasePath = (path: string): string => path.replace(basePathPattern, '/'); +export const getCluster = (path: string): string => { + const strippedPath = stripBasePath(path); + const split = strippedPath.split('/').filter((x) => x); + + return split[0] === 'cluster' ? split[1] : null; +}; + export const getNamespace = (path: string): string => { path = stripBasePath(path); const split = path.split('/').filter((x) => x); @@ -46,6 +54,8 @@ export const getNamespace = (path: string): string => { ns = split[3]; } else if (split[1] === 'ns' && split[2]) { ns = split[2]; + } else if (split[0] === 'cluster' && split[3] === 'ns' && split[4]) { + ns = split[4]; } else { return; } diff --git a/frontend/public/components/utils/resource-link.tsx b/frontend/public/components/utils/resource-link.tsx index 48a9cef62df..c281ab0ee08 100644 --- a/frontend/public/components/utils/resource-link.tsx +++ b/frontend/public/components/utils/resource-link.tsx @@ -2,8 +2,10 @@ import * as _ from 'lodash-es'; import * as React from 'react'; import { Link } from 'react-router-dom'; import * as classNames from 'classnames'; - import { FLAGS } from '@console/shared/src/constants'; +import { getActiveCluster } from '@console/internal/actions/ui'; +import isMultiClusterEnabled from '@console/app/src/utils/isMultiClusterEnabled'; + import { ResourceIcon } from './resource-icon'; import { modelFor, @@ -18,10 +20,21 @@ import { FlagsObject } from '../../reducers/features'; const unknownKinds = new Set(); -export const resourcePathFromModel = (model: K8sKind, name?: string, namespace?: string) => { +export const resourcePathFromModel = ( + model: K8sKind, + name?: string, + namespace?: string, + cluster?: string, +) => { const { plural, namespaced, crd } = model; + const targetCluster = cluster || getActiveCluster(); + + let url = ''; + if (targetCluster && isMultiClusterEnabled()) { + url += `/cluster/${targetCluster}`; + } - let url = '/k8s/'; + url += '/k8s/'; if (!namespaced) { url += 'cluster/';