Skip to content

Commit 12729ec

Browse files
authored
Add default platform integration (#14)
1 parent ff85055 commit 12729ec

15 files changed

+563
-140
lines changed

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,45 @@ The repository contains the following modules:
7373
- **style**: Global styles
7474
- **types**: TypeScript type definitions
7575

76+
## Configuration
77+
78+
### ModularArchConfig
79+
80+
The library supports various configuration options through the `ModularArchConfig` interface:
81+
82+
```typescript
83+
interface ModularArchConfig {
84+
deploymentMode: DeploymentMode;
85+
platformMode: PlatformMode;
86+
URL_PREFIX: string;
87+
BFF_API_VERSION: string;
88+
mandatoryNamespace?: string; // Optional: Force a specific namespace
89+
}
90+
```
91+
92+
#### Mandatory Namespace
93+
94+
The `mandatoryNamespace` option allows you to enforce a specific namespace throughout the application:
95+
96+
```typescript
97+
const config = {
98+
deploymentMode: DeploymentMode.Standalone,
99+
platformMode: PlatformMode.Default,
100+
URL_PREFIX: '/api',
101+
BFF_API_VERSION: 'v1',
102+
mandatoryNamespace: 'production' // Force the use of 'production' namespace
103+
};
104+
```
105+
106+
When `mandatoryNamespace` is set:
107+
108+
- The namespace selector in the UI will be disabled
109+
- All API calls will be restricted to the specified namespace
110+
- Users cannot switch to different namespaces
111+
- The `useNamespaces` hook will only return the mandatory namespace
112+
113+
This is useful for production environments or when you want to restrict users to a specific namespace.
114+
76115
## Development
77116

78117
### Prerequisites

components/NavBar.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
import { SimpleSelect } from '@patternfly/react-templates';
2222
import { BarsIcon } from '@patternfly/react-icons';
2323
import { useNamespaceSelector } from '~/hooks/useNamespaceSelector';
24+
import { useModularArchContext } from '~/hooks/useModularArchContext';
2425
import logoDarkTheme from '~/images/logo-dark-theme.svg';
2526
import { useThemeContext } from '~/hooks/useThemeContext';
2627

@@ -31,10 +32,14 @@ interface NavBarProps {
3132

3233
const NavBar: React.FC<NavBarProps> = ({ username, onLogout }) => {
3334
const { namespaces, preferredNamespace, updatePreferredNamespace } = useNamespaceSelector();
35+
const { config } = useModularArchContext();
3436
const { isMUITheme } = useThemeContext();
3537

3638
const [userMenuOpen, setUserMenuOpen] = React.useState(false);
3739

40+
// Check if mandatory namespace is configured
41+
const isMandatoryNamespace = Boolean(config.mandatoryNamespace);
42+
3843
const options = namespaces.map((namespace) => ({
3944
content: namespace.name,
4045
value: namespace.name,
@@ -75,8 +80,12 @@ const NavBar: React.FC<NavBarProps> = ({ username, onLogout }) => {
7580
<ToolbarItem className="kubeflow-u-namespace-select">
7681
<SimpleSelect
7782
initialOptions={options}
83+
isDisabled={isMandatoryNamespace} // Disable selection when mandatory namespace is set
7884
onSelect={(_ev, selection) => {
79-
updatePreferredNamespace({ name: String(selection) });
85+
// Only allow selection if not mandatory namespace
86+
if (!isMandatoryNamespace) {
87+
updatePreferredNamespace({ name: String(selection) });
88+
}
8089
}}
8190
/>
8291
</ToolbarItem>

components/__tests__/NavBar.test.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import '@testing-library/jest-dom';
2+
import React from 'react';
3+
import { render, screen } from '@testing-library/react';
4+
import * as useFetchStateModule from '~/utilities/useFetchState';
5+
import { ModularArchContextProvider } from '~/context/ModularArchContext';
6+
import { DeploymentMode, PlatformMode } from '~/utilities';
7+
import { ModularArchConfig } from '~/types';
8+
import NavBar from '../NavBar';
9+
10+
// Mock the utilities
11+
jest.mock('~/utilities/useFetchState');
12+
jest.mock('~/api/k8s', () => ({
13+
getNamespaces: jest.fn(() => jest.fn(() => Promise.resolve([]))),
14+
}));
15+
16+
const mockUseFetchState = useFetchStateModule.useFetchState as jest.MockedFunction<
17+
typeof useFetchStateModule.useFetchState
18+
>;
19+
20+
const createMockConfig = (
21+
mandatoryNamespace?: string,
22+
deploymentMode: DeploymentMode = DeploymentMode.Standalone,
23+
platformMode: PlatformMode = PlatformMode.Default,
24+
): ModularArchConfig => ({
25+
deploymentMode,
26+
platformMode,
27+
URL_PREFIX: 'test',
28+
BFF_API_VERSION: 'v1',
29+
...(mandatoryNamespace && { mandatoryNamespace }),
30+
});
31+
32+
const createWrapper = (config: ModularArchConfig) => {
33+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
34+
<ModularArchContextProvider config={config}>{children}</ModularArchContextProvider>
35+
);
36+
Wrapper.displayName = 'TestWrapper';
37+
return Wrapper;
38+
};
39+
40+
describe('NavBar mandatory namespace functionality', () => {
41+
beforeEach(() => {
42+
jest.clearAllMocks();
43+
// Mock fetch for script loading
44+
global.fetch = jest.fn().mockResolvedValue({ ok: false });
45+
});
46+
47+
afterEach(() => {
48+
jest.restoreAllMocks();
49+
// Clean up fetch stub explicitly
50+
// @ts-expect-error – fetch might be undefined in node
51+
delete global.fetch;
52+
});
53+
54+
it('should disable namespace selection when mandatory namespace is set', async () => {
55+
const mandatoryNamespace = 'mandatory-namespace';
56+
const config = createMockConfig(mandatoryNamespace);
57+
const Wrapper = createWrapper(config);
58+
59+
// Mock useFetchState to return only the mandatory namespace
60+
mockUseFetchState.mockReturnValue([[{ name: mandatoryNamespace }], true, undefined, jest.fn()]);
61+
62+
render(<NavBar onLogout={jest.fn()} />, { wrapper: Wrapper });
63+
64+
const namespaceButton = await screen.findByText(mandatoryNamespace);
65+
expect(namespaceButton).toBeInTheDocument();
66+
67+
// The MenuToggle button should be disabled due to mandatory namespace
68+
const menuToggle = namespaceButton.closest('button');
69+
expect(menuToggle).toBeDisabled();
70+
});
71+
72+
it('should allow namespace selection when no mandatory namespace is set', async () => {
73+
const config = createMockConfig();
74+
const Wrapper = createWrapper(config);
75+
76+
const mockNamespaces = [{ name: 'namespace-1' }, { name: 'namespace-2' }];
77+
78+
// Mock useFetchState to return multiple namespaces
79+
mockUseFetchState.mockReturnValue([mockNamespaces, true, undefined, jest.fn()]);
80+
81+
render(<NavBar onLogout={jest.fn()} />, { wrapper: Wrapper });
82+
83+
// Check that the namespace selector is present and enabled
84+
const namespaceButton = screen.getByText('namespace-1');
85+
expect(namespaceButton).toBeInTheDocument();
86+
87+
// The MenuToggle button should be enabled (not disabled)
88+
const menuToggle = namespaceButton.closest('button');
89+
expect(menuToggle).not.toBeDisabled();
90+
});
91+
});

context/ModularArchContext.tsx

Lines changed: 43 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import React, { createContext, useEffect, useState } from 'react';
22
import { Bullseye, Spinner } from '@patternfly/react-core';
33
import { ModularArchConfig, Namespace } from '~/types';
4-
import { DeploymentMode, PlatformMode } from '~/utilities';
5-
import { useFetchState, FetchStateCallbackPromise } from '~/utilities/useFetchState';
6-
import { getNamespaces } from '~/api/k8s';
4+
import { kubeflowScriptLoader, kubeflowNamespaceLoader } from '~/utilities';
5+
import { useNamespacesWithConfig } from '~/hooks/useNamespaces';
76

87
export type ModularArchContextType = {
98
config: ModularArchConfig;
@@ -25,128 +24,75 @@ type ModularArchContextProviderProps = {
2524

2625
export const ModularArchContext = createContext<ModularArchContextType | undefined>(undefined);
2726

28-
declare global {
29-
interface Window {
30-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
31-
centraldashboard: any;
32-
}
33-
}
34-
35-
const loadScript = (src: string, onLoad: () => void, onError: () => void) => {
36-
const script = document.createElement('script');
37-
script.src = src;
38-
script.async = true;
39-
script.onload = onLoad;
40-
script.onerror = onError;
41-
document.head.appendChild(script);
42-
};
43-
4427
export const ModularArchContextProvider: React.FC<ModularArchContextProviderProps> = ({
4528
children,
4629
config,
4730
}) => {
48-
const { deploymentMode, platformMode } = config;
31+
const { deploymentMode, platformMode, mandatoryNamespace } = config;
4932

50-
// Script loading state
5133
const [scriptLoaded, setScriptLoaded] = useState(false);
34+
const [preferredNamespace, setPreferredNamespace] = useState<Namespace | undefined>(undefined);
35+
const [initializationError, setInitializationError] = useState<Error>();
5236

5337
// Namespace-related state
54-
const modArchConfig = React.useMemo(
55-
() => ({
56-
BFF_API_VERSION: config.BFF_API_VERSION,
57-
URL_PREFIX: config.URL_PREFIX,
58-
}),
59-
[config.BFF_API_VERSION, config.URL_PREFIX],
60-
);
61-
62-
const listNamespaces = React.useMemo(() => getNamespaces('', modArchConfig), [modArchConfig]);
63-
64-
const callback = React.useCallback<FetchStateCallbackPromise<Namespace[]>>(
65-
(opts) => {
66-
if (!(deploymentMode === DeploymentMode.Standalone)) {
67-
return Promise.resolve([]);
68-
}
69-
return listNamespaces(opts);
70-
},
71-
[deploymentMode, listNamespaces],
72-
);
73-
74-
const [unsortedNamespaces, isLoaded, error] = useFetchState<Namespace[]>(callback, []);
38+
const [unsortedNamespaces, isLoaded, error] = useNamespacesWithConfig(config);
7539
const namespaces = React.useMemo(
7640
() => unsortedNamespaces.toSorted((a: Namespace, b: Namespace) => a.name.localeCompare(b.name)),
7741
[unsortedNamespaces],
7842
);
79-
const [preferredNamespace, setPreferredNamespace] = useState<Namespace | undefined>(undefined);
80-
const [initializationError, setInitializationError] = useState<Error>();
8143

8244
const firstNamespace = namespaces.length > 0 ? namespaces[0] : null;
8345

84-
// Script loader for kubeflow integration
85-
useEffect(() => {
86-
const scriptUrl = '/dashboard_lib.bundle.js';
87-
88-
if (
89-
!(deploymentMode === DeploymentMode.Integrated) ||
90-
!(platformMode === PlatformMode.Kubeflow)
91-
) {
92-
// eslint-disable-next-line no-console
93-
console.warn('ModularArchContext: Script not loaded, only needed for kubeflow integration');
94-
setScriptLoaded(true);
95-
return;
46+
// Set preferred namespace based on mandatory namespace or first available namespace
47+
const defaultPreferredNamespace = React.useMemo(() => {
48+
if (mandatoryNamespace) {
49+
return { name: mandatoryNamespace };
9650
}
51+
return firstNamespace;
52+
}, [mandatoryNamespace, firstNamespace]);
9753

98-
fetch(scriptUrl, { method: 'HEAD' })
99-
.then((response) => {
100-
if (response.ok) {
101-
loadScript(
102-
scriptUrl,
103-
() => setScriptLoaded(true),
104-
// eslint-disable-next-line no-console
105-
() => console.error('Failed to load the script'),
106-
);
107-
} else {
108-
// eslint-disable-next-line no-console
109-
console.warn('Script not found');
110-
setScriptLoaded(true);
111-
}
112-
})
113-
// eslint-disable-next-line no-console
114-
.catch((err) => console.error('Error checking script existence', err));
54+
// Script loader for kubeflow integration
55+
useEffect(() => {
56+
kubeflowScriptLoader(
57+
deploymentMode,
58+
platformMode,
59+
() => setScriptLoaded(true),
60+
(scriptError) => {
61+
// eslint-disable-next-line no-console
62+
console.error('Error loading kubeflow script:', scriptError);
63+
setScriptLoaded(true); // Still set to true to not block the UI
64+
},
65+
);
11566
}, [deploymentMode, platformMode]);
11667

11768
// Namespace selector for kubeflow integration
11869
useEffect(() => {
119-
if (
120-
deploymentMode === DeploymentMode.Integrated &&
121-
platformMode === PlatformMode.Kubeflow &&
122-
scriptLoaded
123-
) {
124-
// Initialize the central dashboard client
125-
try {
126-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
127-
window.centraldashboard.CentralDashboardEventHandler.init((cdeh: any) => {
128-
// eslint-disable-next-line no-param-reassign
129-
cdeh.onNamespaceSelected = (newNamespace: string) => {
130-
setPreferredNamespace({ name: newNamespace });
131-
};
132-
});
133-
} catch (err) {
134-
/* eslint-disable no-console */
135-
console.error('Failed to initialize central dashboard client', err);
136-
if (err instanceof Error) {
137-
setInitializationError(err);
138-
}
139-
}
70+
// If mandatory namespace is set, don't use kubeflow namespace loader
71+
if (mandatoryNamespace) {
72+
return;
14073
}
141-
}, [deploymentMode, platformMode, scriptLoaded]);
74+
75+
kubeflowNamespaceLoader(
76+
deploymentMode,
77+
platformMode,
78+
scriptLoaded,
79+
(newNamespace: string) => {
80+
setPreferredNamespace({ name: newNamespace });
81+
},
82+
(err: Error) => {
83+
setInitializationError(err);
84+
},
85+
mandatoryNamespace,
86+
);
87+
}, [deploymentMode, platformMode, scriptLoaded, mandatoryNamespace]);
14288

14389
const contextValue = React.useMemo(
14490
() => ({
14591
config,
14692
namespacesLoaded: isLoaded,
14793
namespacesLoadError: error,
14894
namespaces,
149-
preferredNamespace: preferredNamespace ?? firstNamespace ?? undefined,
95+
preferredNamespace: preferredNamespace ?? defaultPreferredNamespace ?? undefined,
15096
updatePreferredNamespace: setPreferredNamespace,
15197
initializationError,
15298
scriptLoaded,
@@ -157,7 +103,7 @@ export const ModularArchContextProvider: React.FC<ModularArchContextProviderProp
157103
error,
158104
namespaces,
159105
preferredNamespace,
160-
firstNamespace,
106+
defaultPreferredNamespace,
161107
initializationError,
162108
scriptLoaded,
163109
],

0 commit comments

Comments
 (0)