diff --git a/frontend/@types/console/console.d.ts b/frontend/@types/console/console.d.ts
new file mode 100644
index 00000000000..9bb05f6c68f
--- /dev/null
+++ b/frontend/@types/console/console.d.ts
@@ -0,0 +1,77 @@
+declare interface Window {
+ SERVER_FLAGS: {
+ copiedCSVsDisabled: boolean;
+ alertManagerBaseURL: string;
+ alertmanagerUserWorkloadBaseURL: string;
+ authDisabled: boolean;
+ basePath: string;
+ branding: string;
+ consoleVersion: string;
+ customLogoURL: string;
+ customLogosConfigured: boolean;
+ customFaviconsConfigured: boolean;
+ customProductName: string;
+ documentationBaseURL: string;
+ kubeAdminLogoutURL: string;
+ kubeAPIServerURL: string;
+ loadTestFactor: number;
+ loginErrorURL: string;
+ loginSuccessURL: string;
+ loginURL: string;
+ logoutRedirect: string;
+ logoutURL: string;
+ prometheusBaseURL: string;
+ prometheusTenancyBaseURL: string;
+ quickStarts: string;
+ releaseVersion: string;
+ inactivityTimeout: number;
+ statuspageID: string;
+ GOARCH: string;
+ GOOS: string;
+ graphqlBaseURL: string;
+ developerCatalogCategories: string;
+ perspectives: string;
+ developerCatalogTypes: string;
+ userSettingsLocation: string;
+ addPage: string; // JSON encoded configuration
+ consolePlugins: string[]; // Console dynamic plugins enabled on the cluster
+ i18nNamespaces: string[]; // Available i18n namespaces
+ quickStarts: string;
+ projectAccessClusterRoles: string;
+ controlPlaneTopology: string;
+ telemetry?: Partial<{
+ // All of the following should be always available on prod env.
+ SEGMENT_API_HOST: string;
+ SEGMENT_JS_HOST: string;
+ // One of the following should be always available on prod env.
+ SEGMENT_API_KEY: string;
+ SEGMENT_PUBLIC_API_KEY: string;
+ DEVSANDBOX_SEGMENT_API_KEY: string;
+ // Optional override for analytics.min.js script URL
+ SEGMENT_JS_URL: string;
+ // Additional telemetry options passed to Console frontend
+ DEBUG: 'true' | 'false';
+ DISABLED: 'true' | 'false';
+ [name: string]: string;
+ }>;
+ nodeArchitectures: string[];
+ nodeOperatingSystems: string[];
+ hubConsoleURL: string;
+ k8sMode: string;
+ techPreview: boolean;
+ capabilities: {
+ name: string;
+ visibility: { state: 'Enabled' | 'Disabled' };
+ }[];
+ };
+ /** (OCPBUGS-46415) Do not override this string! To add new errors please append to windowError if it exists*/
+ windowError?: string;
+ __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: Function;
+ i18n?: {}; // i18next instance, only available in development builds for debugging
+ store?: {}; // Redux store, only available in development builds for debugging
+ pluginStore?: {}; // Console plugin store
+ loadPluginEntry?: Function; // Console plugin entry callback, used to load dynamic plugins
+ webpackSharedScope?: {}; // webpack shared scope object
+ Cypress?: {};
+ monaco?: {};
+}
diff --git a/frontend/@types/console/index.d.ts b/frontend/@types/console/index.d.ts
index 6e157fdf33e..7ef664edd7b 100644
--- a/frontend/@types/console/index.d.ts
+++ b/frontend/@types/console/index.d.ts
@@ -1,5 +1,6 @@
///
///
+///
///
declare module '*.svg' {
@@ -11,83 +12,3 @@ declare module '*.png' {
const value: any;
export default value;
}
-
-declare interface Window {
- SERVER_FLAGS: {
- copiedCSVsDisabled: boolean;
- alertManagerBaseURL: string;
- alertmanagerUserWorkloadBaseURL: string;
- authDisabled: boolean;
- basePath: string;
- branding: string;
- consoleVersion: string;
- customLogoURL: string;
- customLogosConfigured: boolean;
- customFaviconsConfigured: boolean;
- customProductName: string;
- documentationBaseURL: string;
- kubeAPIServerURL: string;
- loadTestFactor: number;
- loginErrorURL: string;
- loginSuccessURL: string;
- loginURL: string;
- logoutRedirect: string;
- logoutURL: string;
- prometheusBaseURL: string;
- prometheusTenancyBaseURL: string;
- quickStarts: string;
- releaseVersion: string;
- inactivityTimeout: number;
- statuspageID: string;
- GOARCH: string;
- GOOS: string;
- graphqlBaseURL: string;
- developerCatalogCategories: string;
- perspectives: string;
- developerCatalogTypes: string;
- userSettingsLocation: string;
- addPage: string; // JSON encoded configuration
- consolePlugins: string[]; // Console dynamic plugins enabled on the cluster
- i18nNamespaces: string[]; // Available i18n namespaces
- quickStarts: string;
- projectAccessClusterRoles: string;
- controlPlaneTopology: string;
- telemetry?: Partial<{
- // All of the following should be always available on prod env.
- SEGMENT_API_HOST: string;
- SEGMENT_JS_HOST: string;
- // One of the following should be always available on prod env.
- SEGMENT_API_KEY: string;
- SEGMENT_PUBLIC_API_KEY: string;
- DEVSANDBOX_SEGMENT_API_KEY: string;
- // Optional override for analytics.min.js script URL
- SEGMENT_JS_URL: string;
- // Additional telemetry options passed to Console frontend
- DEBUG: 'true' | 'false';
- DISABLED: 'true' | 'false';
- [name: string]: string;
- }>;
- nodeArchitectures: string[];
- nodeOperatingSystems: string[];
- hubConsoleURL: string;
- k8sMode: string;
- techPreview: boolean;
- capabilities: {
- name: string;
- visibility: { state: 'Enabled' | 'Disabled' };
- }[];
- };
- /** (OCPBUGS-46415) Do not override this string! To add new errors please append to windowError if it exists*/
- windowError?: string;
- __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: Function;
- i18n?: {}; // i18next instance, only available in development builds for debugging
- store?: {}; // Redux store, only available in development builds for debugging
- pluginStore?: {}; // Console plugin store
- loadPluginEntry?: Function; // Console plugin entry callback, used to load dynamic plugins
- webpackSharedScope?: {}; // webpack shared scope object
- Cypress?: {};
- monaco?: {};
-}
-
-// From https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html
-declare type Diff = Omit;
diff --git a/frontend/packages/console-dynamic-plugin-sdk/@types/sdk/index.d.ts b/frontend/packages/console-dynamic-plugin-sdk/@types/sdk/index.d.ts
index e01cd3c389c..a434905a880 100644
--- a/frontend/packages/console-dynamic-plugin-sdk/@types/sdk/index.d.ts
+++ b/frontend/packages/console-dynamic-plugin-sdk/@types/sdk/index.d.ts
@@ -1,8 +1 @@
-declare interface Window {
- SERVER_FLAGS: {
- basePath: string;
- };
- /** (OCPBUGS-46415) Do not override this string! To add new errors please append to windowError if it exists*/
- windowError?: string;
- __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: Function;
-}
+///
diff --git a/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md b/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md
index d1e19c4d22d..12d600b6a8b 100644
--- a/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md
+++ b/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md
@@ -13,6 +13,7 @@ table in [Console dynamic plugins README](./README.md).
## 4.22.0-prerelease.2 - TBD
- **Breaking**: Removed `pluginID` from the result in `useResolvedExtensions` hook ([CONSOLE-3769], [#15904])
+- **Breaking**: Removed `AppInitSDK` and `useReduxStore` in `app` directory ([CONSOLE-5063], [#16019])
- The following types are now re-exported from `@openshift/dynamic-plugin-sdk` instead of being defined
by Console: `CodeRef`, `EncodedCodeRef`, `LoadedExtension`, and `ResolvedExtension` ([CONSOLE-3769], [#15904])
@@ -250,3 +251,4 @@ table in [Console dynamic plugins README](./README.md).
[#15893]: https://github.com/openshift/console/pull/15893
[#15904]: https://github.com/openshift/console/pull/15904
[#15934]: https://github.com/openshift/console/pull/15934
+[#16019]: https://github.com/openshift/console/pull/16019
diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/AppInitSDK.tsx b/frontend/packages/console-dynamic-plugin-sdk/src/app/AppInitSDK.tsx
deleted file mode 100644
index bce69e5a6d3..00000000000
--- a/frontend/packages/console-dynamic-plugin-sdk/src/app/AppInitSDK.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import type { ReactNode, FC } from 'react';
-import { useEffect } from 'react';
-import { Provider } from 'react-redux';
-import { setUtilsConfig, UtilsConfig } from './configSetup';
-import { initApiDiscovery } from './k8s/api-discovery/api-discovery';
-import { InitApiDiscovery } from './k8s/api-discovery/api-discovery-types';
-import { useReduxStore } from './useReduxStore';
-
-type AppInitSDKProps = {
- children: ReactNode;
- configurations: {
- apiDiscovery?: InitApiDiscovery;
- appFetch: UtilsConfig['appFetch'];
- dynamicPlugins?: () => void;
- };
-};
-
-/**
- * Component for providing store access to the SDK.
- * Add this at app-level to make use of app's redux store and pass configurations prop needed to initialize the app, preferred to have it under Provider.
- * It checks for store instance if present or not.
- * If the store is there then the reference is persisted to be used in SDK else it creates a new store and passes it to the children with the provider
- * @component AppInitSDK
- * @example
- * ```ts
- * return (
- *
- *
- *
- * ...
- *
- *
- * )
- * ```
- */
-const AppInitSDK: FC = ({ children, configurations }) => {
- const { store, storeContextPresent } = useReduxStore();
- useEffect(() => {
- const { appFetch, dynamicPlugins, apiDiscovery = initApiDiscovery } = configurations;
- try {
- setUtilsConfig({ appFetch });
- dynamicPlugins?.();
- apiDiscovery(store);
- } catch (e) {
- // eslint-disable-next-line no-console
- console.warn(e);
- }
- }, [configurations, store]);
-
- return !storeContextPresent ? {children} : <>{children}>;
-};
-
-export default AppInitSDK;
diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/__tests__/AppInitSDK.spec.tsx b/frontend/packages/console-dynamic-plugin-sdk/src/app/__tests__/AppInitSDK.spec.tsx
deleted file mode 100644
index cebe76b2c42..00000000000
--- a/frontend/packages/console-dynamic-plugin-sdk/src/app/__tests__/AppInitSDK.spec.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import '@testing-library/jest-dom';
-import * as reactRedux from 'react-redux';
-import configureMockStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import AppInitSDK from '../AppInitSDK';
-import * as configSetup from '../configSetup';
-import * as apiDiscovery from '../k8s/api-discovery/api-discovery';
-import * as hooks from '../useReduxStore';
-
-jest.mock('react-redux', () => ({
- Provider: jest.fn(({ children }) => children),
-}));
-
-jest.mock('../useReduxStore', () => ({
- useReduxStore: jest.fn(),
-}));
-
-jest.mock('../configSetup', () => ({
- setUtilsConfig: jest.fn(),
-}));
-
-jest.mock('../k8s/api-discovery/api-discovery', () => ({
- initApiDiscovery: jest.fn(),
-}));
-
-const { useReduxStore: useReduxStoreMock } = hooks as jest.Mocked;
-const mockStore = configureMockStore([thunk]);
-const store = mockStore({});
-const mockProvider = (reactRedux as jest.Mocked).Provider;
-const mockConfig = { appFetch: jest.fn() };
-const mockApiDiscoveryConfig = { apiDiscovery: jest.fn(), appFetch: jest.fn() };
-
-describe('AppInitSDK', () => {
- const renderAppInitSDK = (config, storeContext = { store, storeContextPresent: true }) => {
- useReduxStoreMock.mockReturnValue(storeContext);
- return render(
-
- Hello, OpenShift!
- ,
- );
- };
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- it('should not wrap children with Provider', () => {
- renderAppInitSDK(mockConfig, { store, storeContextPresent: true });
-
- expect(mockProvider).not.toHaveBeenCalled();
- expect(screen.getByText('Hello, OpenShift!')).toBeVisible();
- });
-
- it('should wrap children with a Provider if no store context is present', () => {
- renderAppInitSDK(mockConfig, { store, storeContextPresent: false });
-
- expect(mockProvider).toHaveBeenCalled();
- expect(screen.getByText('Hello, OpenShift!')).toBeVisible();
- });
-
- it('should call the useReduxStore hook', () => {
- renderAppInitSDK(mockConfig, { store, storeContextPresent: true });
-
- expect(useReduxStoreMock).toHaveBeenCalledTimes(1);
- });
-
- it('should call the setUtilsConfig utility with the proper config', () => {
- renderAppInitSDK(mockConfig, { store, storeContextPresent: true });
-
- expect(configSetup.setUtilsConfig).toHaveBeenCalledTimes(1);
- expect(configSetup.setUtilsConfig).toHaveBeenCalledWith({ appFetch: mockConfig.appFetch });
- });
-
- it('should call the provided apiDiscovery function if it exists', () => {
- renderAppInitSDK(mockApiDiscoveryConfig, { store, storeContextPresent: true });
-
- expect(mockApiDiscoveryConfig.apiDiscovery).toHaveBeenCalledTimes(1);
- expect(mockApiDiscoveryConfig.apiDiscovery).toHaveBeenCalledWith(store);
- expect(apiDiscovery.initApiDiscovery).not.toHaveBeenCalled();
- });
-
- it('should trigger the default initApiDiscovery if no apiDiscovery function is provided', () => {
- renderAppInitSDK(mockConfig, { store, storeContextPresent: false });
-
- expect(apiDiscovery.initApiDiscovery).toHaveBeenCalledTimes(1);
- expect(apiDiscovery.initApiDiscovery).toHaveBeenCalledWith(store);
- });
-});
diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/__tests__/configSetup.spec.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/__tests__/configSetup.spec.ts
deleted file mode 100644
index 23d79ca4658..00000000000
--- a/frontend/packages/console-dynamic-plugin-sdk/src/app/__tests__/configSetup.spec.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { setUtilsConfig, getUtilsConfig } from '../configSetup';
-
-describe('configSetup', () => {
- it('getUtilsConfig should throw an error if config is not set', () => {
- try {
- getUtilsConfig();
- } catch (e) {
- expect(e.message).toEqual('setUtilsConfig has not been called');
- }
- });
-
- it('getUtilsConfig should provide the config if config has been set', () => {
- const configOptions = { appFetch: jest.fn() };
- setUtilsConfig(configOptions);
- const configData = getUtilsConfig();
- expect(configData).toEqual(configOptions);
- });
-
- it('should throw an error if setUtilsConfig is called and config is already defined', () => {
- try {
- const configOptions = { appFetch: jest.fn() };
- setUtilsConfig(configOptions);
- } catch (e) {
- expect(e.message).toEqual('setUtilsConfig has already been called');
- }
- });
-});
diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/configSetup.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/configSetup.ts
deleted file mode 100644
index 0e86f65074d..00000000000
--- a/frontend/packages/console-dynamic-plugin-sdk/src/app/configSetup.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-export type UtilsConfig = {
- /**
- * Resource fetch implementation provided by the host application.
- *
- * Applications must validate the response before resolving the Promise.
- *
- * If the request cannot be completed successfully, the Promise should be rejected
- * with an appropriate error.
- */
- appFetch: (url: string, options?: RequestInit) => Promise;
-};
-
-let config: UtilsConfig | undefined;
-
-/**
- * Set the {@link UtilsConfig} reference.
- *
- * This must be done before using any of the Kubernetes utilities.
- */
-export const setUtilsConfig = (c: UtilsConfig) => {
- if (config !== undefined) {
- throw new Error('setUtilsConfig has already been called');
- }
-
- config = Object.freeze({ ...c });
-};
-
-/**
- * Get the {@link UtilsConfig} reference.
- *
- * Throws an error if the reference isn't already set.
- */
-export const getUtilsConfig = (): UtilsConfig => {
- if (config === undefined) {
- throw new Error('setUtilsConfig has not been called');
- }
-
- return config;
-};
diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/index.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/index.ts
index fba49e4b67d..bade1ec813a 100644
--- a/frontend/packages/console-dynamic-plugin-sdk/src/app/index.ts
+++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/index.ts
@@ -1,4 +1,3 @@
-export { default as AppInitSDK } from './AppInitSDK';
export { SDKReducers } from './redux';
export * from './components';
export * from './core';
diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/useReduxStore.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/useReduxStore.ts
deleted file mode 100644
index 37248272958..00000000000
--- a/frontend/packages/console-dynamic-plugin-sdk/src/app/useReduxStore.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { useState, useMemo } from 'react';
-import { useStore } from 'react-redux';
-import { applyMiddleware, combineReducers, createStore, compose, Store } from 'redux';
-import thunk from 'redux-thunk';
-import { SDKReducers } from './redux';
-import { SDKStoreState } from './redux-types';
-import storeHandler from './storeHandler';
-
-const composeEnhancers =
- (process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose;
-
-/**
- * `useReduxStore` will provide the store instance if present or else create one along with info if the context was present.
- *
- * @example
- * ```ts
- * function Component () {
- * const {store, storeContextPresent} = useReduxStore()
- * return ...
- * }
- * ```
- */
-export const useReduxStore = (): { store: Store; storeContextPresent: boolean } => {
- const storeContext = useStore();
- const [storeContextPresent, setStoreContextPresent] = useState(false);
- const store = useMemo(() => {
- // check if store exists and if not create it
- if (storeContext) {
- setStoreContextPresent(true);
- storeHandler.setStore(storeContext);
- } else {
- // eslint-disable-next-line no-console
- console.log('Creating the SDK redux store');
- setStoreContextPresent(false);
- const storeInstance = createStore(
- combineReducers(SDKReducers),
- {},
- composeEnhancers(applyMiddleware(thunk)),
- );
- storeHandler.setStore(storeInstance);
- }
- return storeHandler.getStore();
- }, [storeContext]);
-
- return { store, storeContextPresent };
-};
diff --git a/frontend/public/__tests__/co-fetch.spec.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/fetch/__tests__/console-fetch.spec.ts
similarity index 89%
rename from frontend/public/__tests__/co-fetch.spec.ts
rename to frontend/packages/console-dynamic-plugin-sdk/src/utils/fetch/__tests__/console-fetch.spec.ts
index d90e94898eb..12d298c40f6 100644
--- a/frontend/public/__tests__/co-fetch.spec.ts
+++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/fetch/__tests__/console-fetch.spec.ts
@@ -1,9 +1,8 @@
-import { RetryError } from '@console/dynamic-plugin-sdk/src/utils/error/http-error';
-import { consoleFetch } from '@console/dynamic-plugin-sdk/src/utils/fetch';
-import { setUtilsConfig } from '@console/dynamic-plugin-sdk/src/app/configSetup';
-import { shouldLogout, validateStatus, appInternalFetch } from '../co-fetch';
+import { RetryError } from '../../error/http-error';
+import { consoleFetch } from '../console-fetch';
+import { shouldLogout, validateStatus } from '../console-fetch-utils';
-describe('coFetch', () => {
+describe('consoleFetch', () => {
const json = async () => ({
details: {
kind: 'clusterresourcequotas',
@@ -80,7 +79,6 @@ describe('coFetch', () => {
window.fetch = jest.fn(() =>
Promise.resolve({ status: 404, headers: emptyHeaders } as Response),
);
- setUtilsConfig({ appFetch: appInternalFetch });
try {
await consoleFetch('');
} catch {
diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/fetch/console-fetch-utils.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/fetch/console-fetch-utils.ts
index be80ef025b5..fc3c5cfe5d7 100644
--- a/frontend/packages/console-dynamic-plugin-sdk/src/utils/fetch/console-fetch-utils.ts
+++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/fetch/console-fetch-utils.ts
@@ -1,5 +1,6 @@
import { getImpersonate } from '../../app/core/reducers';
import storeHandler from '../../app/storeHandler';
+import { HttpError, RetryError } from '../error/http-error';
type ConsoleRequestHeaders = {
'Impersonate-Group'?: string | string[];
@@ -86,3 +87,136 @@ export const normalizeConsoleHeaders = (
return normalized;
};
+
+/**
+ * A utility function to apply console-specific headers to the provided fetch options.
+ * @returns Modified `options` object with additional request headers.
+ */
+export const applyConsoleHeaders = (url: string, options: RequestInit): RequestInit => {
+ const consoleHeaders = getConsoleRequestHeaders();
+
+ if (!options.headers) {
+ options.headers = {};
+ }
+
+ // Apply console headers, handling array values for multiple headers
+ Object.entries(consoleHeaders || {}).forEach(([key, value]) => {
+ if (Array.isArray(value)) {
+ // For multiple Impersonate-Group headers, we need special handling
+ // because fetch() API combines them into a single comma-separated header
+ // which doesn't work for Kubernetes impersonation
+ if (key === 'Impersonate-Group') {
+ // Send as a special header that the backend will split
+ options.headers['X-Console-Impersonate-Groups'] = value.join(',');
+ } else {
+ // For other array headers, store as array
+ options.headers[key] = value;
+ }
+ } else if (value) {
+ options.headers[key] = value;
+ }
+ });
+
+ // X-CSRFToken is used only for non-GET requests targeting bridge
+ if (options.method === 'GET' || url.indexOf('://') >= 0) {
+ delete options.headers['X-CSRFToken'];
+ }
+
+ return options;
+};
+
+// TODO: url can be url or path, but shouldLogout only handles paths
+export const shouldLogout = (url: string): boolean => {
+ const k8sRegex = new RegExp(`^${window.SERVER_FLAGS.basePath}api/kubernetes/`);
+ // 401 from k8s. show logout screen
+ if (k8sRegex.test(url)) {
+ // Don't let 401s from proxied services log out users
+ const proxyRegex = new RegExp(`^${window.SERVER_FLAGS.basePath}api/kubernetes/api/v1/proxy/`);
+ if (proxyRegex.test(url)) {
+ return false;
+ }
+ const serviceRegex = new RegExp(
+ `^${window.SERVER_FLAGS.basePath}api/kubernetes/api/v1/namespaces/\\w+/services/\\w+/proxy/`,
+ );
+ if (serviceRegex.test(url)) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+};
+
+export const validateStatus = async (
+ response: Response,
+ url: string,
+ method: string,
+ retry: boolean,
+) => {
+ if (response.ok || response.status === 304) {
+ return response;
+ }
+
+ if (retry && response.status === 429) {
+ throw new RetryError();
+ }
+
+ if (response.status === 401 && shouldLogout(url)) {
+ const next = window.location.pathname + window.location.search + window.location.hash;
+ // We can't use regular import from outside this package, so a dynamic import is required
+ // This also breaks a nasty cycle - authSvc.logout calls coFetch (which calls validateStatus)
+ import('@console/internal/module/auth')
+ .then((m) => m.authSvc)
+ .then((authSvc) => {
+ authSvc.logout(next);
+ })
+ .catch((e) => {
+ // eslint-disable-next-line no-console
+ console.error('Error during logout after 401 response', e);
+ });
+ }
+
+ const contentType = response.headers.get('content-type');
+ if (!contentType || contentType.indexOf('json') === -1) {
+ throw new HttpError(response.statusText, response.status, response);
+ }
+
+ if (response.status === 403) {
+ return response.json().then((json) => {
+ throw new HttpError(
+ json.message || 'Access denied due to cluster policy.',
+ response.status,
+ response,
+ json,
+ );
+ });
+ }
+
+ return response.json().then((json) => {
+ // retry 409 conflict errors due to ClustResourceQuota / ResourceQuota
+ // https://bugzilla.redhat.com/show_bug.cgi?id=1920699
+ if (
+ retry &&
+ method === 'POST' &&
+ response.status === 409 &&
+ ['resourcequotas', 'clusterresourcequotas'].includes(json.details?.kind)
+ ) {
+ throw new RetryError();
+ }
+ const cause = json.details?.causes?.[0];
+ let reason;
+ if (cause) {
+ reason = `Error "${cause.message}" for field "${cause.field}".`;
+ }
+ if (!reason) {
+ reason = json.message;
+ }
+ if (!reason) {
+ reason = json.error;
+ }
+ if (!reason) {
+ reason = response.statusText;
+ }
+
+ throw new HttpError(reason, response.status, response, json);
+ });
+};
diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/fetch/console-fetch.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/fetch/console-fetch.ts
index 94199c4b80d..a1c5243d887 100644
--- a/frontend/packages/console-dynamic-plugin-sdk/src/utils/fetch/console-fetch.ts
+++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/fetch/console-fetch.ts
@@ -1,31 +1,67 @@
import * as _ from 'lodash';
-import { getUtilsConfig } from '../../app/configSetup';
import { setAdmissionWebhookWarning } from '../../app/core/actions';
import storeHandler from '../../app/storeHandler';
-import { ConsoleFetchText, ConsoleFetchJSON, ConsoleFetch } from '../../extensions/console-types';
-import { TimeoutError } from '../error/http-error';
-import { getConsoleRequestHeaders, normalizeConsoleHeaders } from './console-fetch-utils';
+import { ConsoleFetchJSON, ConsoleFetchText, ConsoleFetch } from '../../extensions/console-types';
+import { RetryError, TimeoutError } from '../error/http-error';
+import {
+ applyConsoleHeaders,
+ getConsoleRequestHeaders,
+ normalizeConsoleHeaders,
+ validateStatus,
+} from './console-fetch-utils';
+
+const initDefaults = {
+ headers: {},
+ credentials: 'same-origin',
+};
/**
* A custom wrapper around `fetch` that adds console-specific headers and allows for retries and timeouts.
* It also validates the response status code and throws an appropriate error or logs out the user if required.
- * @param url The URL to fetch
- * @param options The options to pass to fetch
- * @param timeout The timeout in milliseconds
+ * @param url - The URL to fetch
+ * @param options - The options to pass to fetch
+ * @param timeout - The timeout in milliseconds
* @returns A promise that resolves to the response.
*/
export const consoleFetch: ConsoleFetch = async (url, options = {}, timeout = 60000) => {
- const fetchPromise = getUtilsConfig().appFetch(url, options);
+ const op1 = applyConsoleHeaders(url, options);
+ const allOptions = _.defaultsDeep({}, initDefaults, op1);
+
+ const fetchPromise = async () => {
+ let res: Response;
+ let attempt = 0;
+ let retry = true;
+
+ while (retry) {
+ retry = false;
+ attempt++;
+ try {
+ // eslint-disable-next-line no-await-in-loop, no-loop-func
+ res = await fetch(url, allOptions).then((resp) =>
+ validateStatus(resp, url, allOptions.method, attempt < 3),
+ );
+ } catch (e) {
+ if (e instanceof RetryError) {
+ retry = true;
+ } else {
+ // eslint-disable-next-line no-console
+ console.warn(`consoleFetch failed for url ${url}`, e);
+ throw e;
+ }
+ }
+ }
+ return res;
+ };
if (timeout <= 0) {
- return fetchPromise;
+ return fetchPromise();
}
- const timeoutPromise = new Promise((resolve, reject) => {
+ const timeoutPromise = new Promise((_resolve, reject) => {
setTimeout(() => reject(new TimeoutError(url, timeout)), timeout);
});
- return Promise.race([fetchPromise, timeoutPromise]);
+ return Promise.race([fetchPromise(), timeoutPromise]);
};
const parseData = async (response) => {
diff --git a/frontend/public/co-fetch.ts b/frontend/public/co-fetch.ts
index 499a0d08d5b..3328b3eb4f3 100644
--- a/frontend/public/co-fetch.ts
+++ b/frontend/public/co-fetch.ts
@@ -1,159 +1,3 @@
-import * as _ from 'lodash';
-import { HttpError, RetryError } from '@console/dynamic-plugin-sdk/src/utils/error/http-error';
-import { authSvc } from './module/auth';
-import { getConsoleRequestHeaders } from '@console/dynamic-plugin-sdk/src/utils/fetch/console-fetch-utils';
-
-export const applyConsoleHeaders = (url, options) => {
- const consoleHeaders = getConsoleRequestHeaders();
-
- if (!options.headers) {
- options.headers = {};
- }
-
- // Apply console headers, handling array values for multiple headers
- Object.entries(consoleHeaders || {}).forEach(([key, value]) => {
- if (Array.isArray(value)) {
- // For multiple Impersonate-Group headers, we need special handling
- // because fetch() API combines them into a single comma-separated header
- // which doesn't work for Kubernetes impersonation
- if (key === 'Impersonate-Group') {
- // Send as a special header that the backend will split
- options.headers['X-Console-Impersonate-Groups'] = value.join(',');
- } else {
- // For other array headers, store as array
- options.headers[key] = value;
- }
- } else if (value) {
- options.headers[key] = value;
- }
- });
-
- // X-CSRFToken is used only for non-GET requests targeting bridge
- if (options.method === 'GET' || url.indexOf('://') >= 0) {
- delete options.headers['X-CSRFToken'];
- }
- return options;
-};
-
-// TODO: url can be url or path, but shouldLogout only handles paths
-export const shouldLogout = (url: string): boolean => {
- const k8sRegex = new RegExp(`^${window.SERVER_FLAGS.basePath}api/kubernetes/`);
- // 401 from k8s. show logout screen
- if (k8sRegex.test(url)) {
- // Don't let 401s from proxied services log out users
- const proxyRegex = new RegExp(`^${window.SERVER_FLAGS.basePath}api/kubernetes/api/v1/proxy/`);
- if (proxyRegex.test(url)) {
- return false;
- }
- const serviceRegex = new RegExp(
- `^${window.SERVER_FLAGS.basePath}api/kubernetes/api/v1/namespaces/\\w+/services/\\w+/proxy/`,
- );
- if (serviceRegex.test(url)) {
- return false;
- }
- return true;
- }
- return false;
-};
-
-export const validateStatus = async (
- response: Response,
- url: string,
- method: string,
- retry: boolean,
-) => {
- if (response.ok || response.status === 304) {
- return response;
- }
-
- if (retry && response.status === 429) {
- throw new RetryError();
- }
-
- if (response.status === 401 && shouldLogout(url)) {
- const next = window.location.pathname + window.location.search + window.location.hash;
- authSvc.logout(next);
- }
-
- const contentType = response.headers.get('content-type');
- if (!contentType || contentType.indexOf('json') === -1) {
- throw new HttpError(response.statusText, response.status, response);
- }
-
- if (response.status === 403) {
- return response.json().then((json) => {
- throw new HttpError(
- json.message || 'Access denied due to cluster policy.',
- response.status,
- response,
- json,
- );
- });
- }
-
- return response.json().then((json) => {
- // retry 409 conflict errors due to ClustResourceQuota / ResourceQuota
- // https://bugzilla.redhat.com/show_bug.cgi?id=1920699
- if (
- retry &&
- method === 'POST' &&
- response.status === 409 &&
- ['resourcequotas', 'clusterresourcequotas'].includes(json.details?.kind)
- ) {
- throw new RetryError();
- }
- const cause = json.details?.causes?.[0];
- let reason;
- if (cause) {
- reason = `Error "${cause.message}" for field "${cause.field}".`;
- }
- if (!reason) {
- reason = json.message;
- }
- if (!reason) {
- reason = json.error;
- }
- if (!reason) {
- reason = response.statusText;
- }
-
- throw new HttpError(reason, response.status, response, json);
- });
-};
-
-const initDefaults = {
- headers: {},
- credentials: 'same-origin',
-};
-
-export const appInternalFetch = async (url: string, options: RequestInit): Promise => {
- let attempt = 0;
- let response: Response;
- let retry = true;
-
- const op1 = applyConsoleHeaders(url, options);
- const allOptions = _.defaultsDeep({}, initDefaults, op1);
-
- while (retry) {
- retry = false;
- attempt++;
- try {
- response = await fetch(url, allOptions).then((resp) =>
- validateStatus(resp, url, allOptions.method, attempt < 3),
- );
- } catch (e) {
- if (e instanceof RetryError) {
- retry = true;
- } else {
- // eslint-disable-next-line no-console
- console.warn(`consoleFetch failed for url ${url}`, e);
- throw e;
- }
- }
- }
- return response;
-};
-
export {
consoleFetch as coFetch,
consoleFetchJSON as coFetchJSON,
diff --git a/frontend/public/components/app.tsx b/frontend/public/components/app.tsx
index 2857c69ca6d..2849d9b5670 100644
--- a/frontend/public/components/app.tsx
+++ b/frontend/public/components/app.tsx
@@ -13,7 +13,6 @@ import store, { applyReduxExtensions, RootState } from '../redux';
import { useTranslation } from 'react-i18next';
import type { LoadedAndResolvedExtension } from '@openshift/dynamic-plugin-sdk';
import { PluginStoreProvider } from '@openshift/dynamic-plugin-sdk';
-import { appInternalFetch } from '../co-fetch';
import { detectFeatures } from '../actions/features';
import { setFlag } from '../actions/flags';
import AppContents from './app-contents';
@@ -41,13 +40,11 @@ import {
isContextProvider,
isReduxReducer,
isStandaloneRoutePage,
- AppInitSDK,
getUser,
useActivePerspective,
ReduxReducer,
ContextProvider,
} from '@console/dynamic-plugin-sdk';
-import { initConsolePlugins } from '@console/dynamic-plugin-sdk/src/runtime/plugin-init';
import { GuidedTour } from '@console/app/src/components/tour';
import { QuickStartDrawer } from '@console/app/src/components/quick-starts/QuickStartDrawer';
import { ModalProvider } from '@console/dynamic-plugin-sdk/src/app/modal-support/ModalProvider';
@@ -453,16 +450,6 @@ const updateSwaggerDefinitionContinual = () => {
}, 5 * 60 * 1000);
};
-/**
- * loading dynamic plugins from PluginStore has a dependency on coFetch, which
- * only works after `AppInitSDK` has set the fetch implementation.
- *
- * TODO: Find a way to load this earlier in the app lifecycle
- * TODO: Revert HAC-375 so coFetch isn't sourced via dependency injection (???)
- */
-const initDynamicPlugins = () => {
- initConsolePlugins(pluginStore);
-};
// Load cached API resources from localStorage to speed up page load.
const initApiDiscovery = (storeInstance) => {
getCachedResources()
@@ -481,6 +468,8 @@ graphQLReady.onReady(() => {
const { productName } = getBrandingDetails();
store.dispatch(detectFeatures());
+ initApiDiscovery(store);
+
// Global timer to ensure all components update in sync
setInterval(() => store.dispatch(UIActions.updateTimestamps(Date.now())), 10000);
@@ -542,19 +531,11 @@ graphQLReady.onReady(() => {
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/frontend/public/module/auth.js b/frontend/public/module/auth.ts
similarity index 91%
rename from frontend/public/module/auth.js
rename to frontend/public/module/auth.ts
index 2a02f787b5f..2311e804f1c 100644
--- a/frontend/public/module/auth.js
+++ b/frontend/public/module/auth.ts
@@ -15,18 +15,18 @@ export const LOGIN_ERROR_PATH = loginErrorURL
? new URL(loginErrorURL, window.location.href).pathname
: '';
-const isLoginErrorPath = (path) => path && path === LOGIN_ERROR_PATH;
+const isLoginErrorPath = (path: string) => path && path === LOGIN_ERROR_PATH;
-const loginState = (key) => localStorage.getItem(key);
+const loginState = (key: string) => localStorage.getItem(key);
-const loginStateItem = (key) => loginState(key);
+const loginStateItem = (key: string) => loginState(key);
const userID = 'userID';
const name = 'name';
const email = 'email';
const clearLocalStorageKeys = [userID, name, email];
-const setNext = (next) => {
+const setNext = (next: string) => {
if (!next) {
return;
}
@@ -40,7 +40,7 @@ const setNext = (next) => {
}
};
-const clearLocalStorage = (keys) => {
+const clearLocalStorage = (keys: string[]) => {
keys.forEach((key) => {
try {
localStorage.removeItem(key);
@@ -123,7 +123,7 @@ export const authSvc = {
// Ensure that we don't redirect to the current URL in a loop
// when using local bridge in development mode without authorization.
if (![window.location.href, window.location.pathname].includes(loginURL)) {
- window.location = loginURL;
+ window.location.assign(loginURL);
}
},
};
diff --git a/frontend/public/plugins.ts b/frontend/public/plugins.ts
index cfe176b9893..129de945ebc 100644
--- a/frontend/public/plugins.ts
+++ b/frontend/public/plugins.ts
@@ -9,6 +9,7 @@ import { consoleFetch } from '@console/dynamic-plugin-sdk/src/utils/fetch/consol
import { ValidationResult } from '@console/dynamic-plugin-sdk/src/validation/ValidationResult';
import { REMOTE_ENTRY_CALLBACK } from '@console/dynamic-plugin-sdk/src/constants';
import { noop } from 'lodash';
+import { initConsolePlugins } from '@console/dynamic-plugin-sdk/src/runtime/plugin-init';
/**
* Set by `console-operator` or `./bin/bridge -release-version`. If this is
@@ -115,6 +116,8 @@ export const pluginStore = new PluginStore({
localPlugins.forEach((plugin) => pluginStore.loadPlugin(plugin));
+initConsolePlugins(pluginStore);
+
/** Redux middleware that updates PluginStore FeatureFlags when redux actions are dispatched. */
export const featureFlagMiddleware: Middleware<{}, RootState> = (s) => {
let prevFlags: RootState['FLAGS'] | undefined;
diff --git a/frontend/public/redux.ts b/frontend/public/redux.ts
index 9fa683bea99..506405ea9d1 100644
--- a/frontend/public/redux.ts
+++ b/frontend/public/redux.ts
@@ -13,6 +13,7 @@ import { featureReducer, featureReducerName } from './reducers/features';
import ObserveReducers, { ObserveState } from './reducers/observe';
import UIReducers, { UIState } from './reducers/ui';
import { dashboardsReducer, DashboardsState } from './reducers/dashboards';
+import storeHandler from '@console/dynamic-plugin-sdk/src/app/storeHandler';
const composeEnhancers =
(process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose;
@@ -41,6 +42,9 @@ const store = createStore(
composeEnhancers(applyMiddleware(thunk, featureFlagMiddleware)),
);
+// Provides redux store object to SDK components that can't import from here
+storeHandler.setStore(store);
+
export const applyReduxExtensions = (reducerExtensions: ResolvedExtension[]) => {
const pluginReducers: ReducersMapObject = {};