From 35104149181c7de6f19543e19661b2c89f53eb3c Mon Sep 17 00:00:00 2001 From: Vojtech Szocs Date: Fri, 6 Mar 2026 18:19:55 +0000 Subject: [PATCH 1/3] CONSOLE-5065: Clean up Console extension code reference processing --- frontend/.yarnrc.yml | 4 + frontend/package.json | 2 +- .../CHANGELOG-core.md | 8 +- .../console-dynamic-plugin-sdk/docs/api.md | 12 ++- .../src/api/dynamic-core-api.ts | 33 +++++-- .../src/api/useResolvedExtensions.ts | 45 ++------- .../__tests__/coderef-resolver.spec.ts | 38 -------- .../src/coderefs/coderef-resolver.ts | 97 ------------------- .../src/extensions/console-types.ts | 4 +- .../console-dynamic-plugin-sdk/src/types.ts | 2 +- .../src/validation/ExtensionValidator.ts | 27 +++--- .../src/api/useExtensions.ts | 6 +- .../src/codegen/local-plugins.ts | 14 ++- .../__tests__/useCatalogExtensions.spec.ts | 2 +- .../src/hooks/create-resource-hook.ts | 4 +- .../useDetailsItemExtensionsForResource.ts | 6 +- frontend/yarn.lock | 10 +- 17 files changed, 88 insertions(+), 226 deletions(-) delete mode 100644 frontend/packages/console-dynamic-plugin-sdk/src/coderefs/__tests__/coderef-resolver.spec.ts delete mode 100644 frontend/packages/console-dynamic-plugin-sdk/src/coderefs/coderef-resolver.ts diff --git a/frontend/.yarnrc.yml b/frontend/.yarnrc.yml index 7db9b30059a..4feef5fc40a 100644 --- a/frontend/.yarnrc.yml +++ b/frontend/.yarnrc.yml @@ -10,4 +10,8 @@ nodeLinker: node-modules npmMinimalAgeGate: 3d +npmPreapprovedPackages: + - "@openshift/*" + - "@patternfly/*" + yarnPath: .yarn/releases/yarn-4.12.0.cjs diff --git a/frontend/package.json b/frontend/package.json index 8ae3956441a..78bdd928f39 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -149,7 +149,7 @@ ] }, "dependencies": { - "@openshift/dynamic-plugin-sdk": "^8.0.0", + "@openshift/dynamic-plugin-sdk": "file:../../dynamic-plugin-sdk/packages/lib-core", "@openshift/dynamic-plugin-sdk-webpack": "^5.1.0", "@patternfly/patternfly": "~6.4.0", "@patternfly/quickstarts": "~6.4.0", diff --git a/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md b/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md index 488fa25d1f5..9a5f6d86a4e 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md +++ b/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md @@ -12,13 +12,14 @@ table in [Console dynamic plugins README](./README.md). ## 4.22.0-prerelease.2 - TBD +- **Breaking**: `useResolvedExtensions` hook now supports a single predicate parameter ([#16115]) - **Breaking**: Removed `pluginID` from the result in `useResolvedExtensions` hook ([CONSOLE-3769], [#15904]) - **Breaking**: Removed `AppInitSDK` and `useReduxStore` in `app` directory ([CONSOLE-5063], [#16019]) - **Deprecated**: `useUserSettings` hook has been renamed to `useUserPreference` for consistency ([OCPBUGS-44612], [#16057]) - **Type breaking**: Changed `useDeleteModal` hook's `redirectTo` parameter type from `LocationDescriptor` (from `history`) to `To` (from `react-router-dom-v5-compat`) ([CONSOLE-4990], [#15959]) - **Type breaking**: Changed `FileUploadHandler` return type from `void` to `To | void`. Handlers can now return a path (from `react-router-dom-v5-compat`) for programmatic navigation instead of calling history methods directly ([CONSOLE-4990], [#15959]) - 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]) + locally: `CodeRef`, `EncodedCodeRef`, `LoadedExtension`, `ResolvedExtension` and `ExtensionPredicate` ([CONSOLE-3769], [#15904], [#16115]) ## 4.22.0-prerelease.1 - 2025-01-21 @@ -50,7 +51,7 @@ table in [Console dynamic plugins README](./README.md). - **Type breaking**: Fix `popupComponent` prop type in extension `console.dashboards/overview/health/resource` ([CONSOLE-4796], [#15526]) - **Type breaking**: `AlwaysOnExtension` and `ModelDefinition` types are removed from `api/common-types`. ([CONSOLE-3769], [#15509]) - The following types are now re-exported from `@openshift/dynamic-plugin-sdk` instead of being defined - locally: `ExtensionFlags`, `ExtensionTypeGuard`, `ResolvedCodeRefProperties`, `RemoteEntryModule`, and `Update`. ([CONSOLE-4840], [#15509], [#15671]) + locally: `ExtensionFlags`, `ExtensionTypeGuard`, `ResolvedCodeRefProperties`, `RemoteEntryModule`, and `Update` ([CONSOLE-4840], [#15509], [#15671]) - Add optional `fetch` property to extension `console.dashboards/overview/health/url` ([CONSOLE-4796], [#15526]) - Add optional `infrastructure` parameter to `PrometheusHealthHandler` type ([CONSOLE-4796], [#15526]) - Allow `K8sResourceKind` in `TopologyDataObject`, `TopologyResourcesObject`, and `OverviewItem` types ([CONSOLE-4840], [#15699]) @@ -264,6 +265,7 @@ 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 +[#15959]: https://github.com/openshift/console/pull/15959 [#16019]: https://github.com/openshift/console/pull/16019 [#16057]: https://github.com/openshift/console/pull/16057 -[#15959]: https://github.com/openshift/console/pull/15959 +[#16115]: https://github.com/openshift/console/pull/16115 diff --git a/frontend/packages/console-dynamic-plugin-sdk/docs/api.md b/frontend/packages/console-dynamic-plugin-sdk/docs/api.md index 525c798e0e7..36d76e998e0 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/docs/api.md +++ b/frontend/packages/console-dynamic-plugin-sdk/docs/api.md @@ -3096,14 +3096,16 @@ Quick start context values object. ### Summary -React hook for consuming Console extensions with resolved `CodeRef` properties.
This hook accepts the same argument(s) as `useExtensions` hook and returns an adapted list of extension instances, resolving all code references within each extension's properties.
Initially, the hook returns an empty array. Once the resolution is complete, the React component is re-rendered with the hook returning an adapted list of extensions.
When the list of matching extensions changes, the resolution is restarted. The hook will continue to return the previous result until the resolution completes.
The hook's result elements are guaranteed to be referentially stable across re-renders. +React hook for consuming Console extensions containing code references.

Initially, this hook returns a result tuple `[resolvedExtensions: [], resolved: false, errors: []]`.

Once the resolution is complete, this hook re-renders the component with a result tuple containing
all matching extensions that had their code references resolved successfully along with any errors
that occurred during the process.

When the list of matching extensions changes, the resolution is restarted. In such case, the hook
will _not_ re-render the component with empty initial result since it's preferable to use existing
state until the current resolution completes.

The hook's result is guaranteed to be referentially stable across re-renders, assuming referential
stability of the `predicate` parameter. ### Example ```ts -const [navItemExtensions, navItemsResolved] = useResolvedExtensions(isNavItem); -// process adapted extensions and render your component +const Example = () => { + const [navItems, navItemsResolved] = useResolvedExtensions(isNavItem); + // process extensions and render your component +}; ``` @@ -3112,13 +3114,13 @@ const [navItemExtensions, navItemsResolved] = useResolvedExtensions(isN | Parameter Name | Description | | -------------- | ----------- | -| `typeGuards` | A list of callbacks that each accept a dynamic plugin extension as an argument and return a boolean flag indicating whether or not the extension meets desired type constraints | +| `predicate` | Predicate (type guard) to filter extensions of a specific type. | ### Returns -Tuple containing a list of adapted extension instances with resolved code references, a boolean flag indicating whether the resolution is complete, and a list of errors detected during the resolution. +Tuple `[resolvedExtensions, resolved, errors]` containing a list of matching extensions,
a boolean flag indicating whether the resolution is complete, and a list of errors detected during
the resolution. ### Source diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/api/dynamic-core-api.ts b/frontend/packages/console-dynamic-plugin-sdk/src/api/dynamic-core-api.ts index 7753294ec07..52479b7977f 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/api/dynamic-core-api.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/api/dynamic-core-api.ts @@ -46,18 +46,33 @@ export * from './common-types'; export * from './utils'; /** - * React hook for consuming Console extensions with resolved `CodeRef` properties. - * This hook accepts the same argument(s) as `useExtensions` hook and returns an adapted list of extension instances, resolving all code references within each extension's properties. - * Initially, the hook returns an empty array. Once the resolution is complete, the React component is re-rendered with the hook returning an adapted list of extensions. - * When the list of matching extensions changes, the resolution is restarted. The hook will continue to return the previous result until the resolution completes. - * The hook's result elements are guaranteed to be referentially stable across re-renders. + * React hook for consuming Console extensions containing code references. + * + * Initially, this hook returns a result tuple `[resolvedExtensions: [], resolved: false, errors: []]`. + * + * Once the resolution is complete, this hook re-renders the component with a result tuple containing + * all matching extensions that had their code references resolved successfully along with any errors + * that occurred during the process. + * + * When the list of matching extensions changes, the resolution is restarted. In such case, the hook + * will _not_ re-render the component with empty initial result since it's preferable to use existing + * state until the current resolution completes. + * + * The hook's result is guaranteed to be referentially stable across re-renders, assuming referential + * stability of the `predicate` parameter. + * * @example * ```ts - * const [navItemExtensions, navItemsResolved] = useResolvedExtensions(isNavItem); - * // process adapted extensions and render your component + * const Example = () => { + * const [navItems, navItemsResolved] = useResolvedExtensions(isNavItem); + * // process extensions and render your component + * }; * ``` - * @param typeGuards A list of callbacks that each accept a dynamic plugin extension as an argument and return a boolean flag indicating whether or not the extension meets desired type constraints - * @returns Tuple containing a list of adapted extension instances with resolved code references, a boolean flag indicating whether the resolution is complete, and a list of errors detected during the resolution. + * + * @param predicate Predicate (type guard) to filter extensions of a specific type. + * @returns Tuple `[resolvedExtensions, resolved, errors]` containing a list of matching extensions, + * a boolean flag indicating whether the resolution is complete, and a list of errors detected during + * the resolution. */ export const useResolvedExtensions: UseResolvedExtensions = require('@console/dynamic-plugin-sdk/src/api/useResolvedExtensions') .useResolvedExtensions; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/api/useResolvedExtensions.ts b/frontend/packages/console-dynamic-plugin-sdk/src/api/useResolvedExtensions.ts index 2d005213dba..d66e9ed8159 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/api/useResolvedExtensions.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/api/useResolvedExtensions.ts @@ -1,42 +1,15 @@ -import { useState, useEffect } from 'react'; +import type { UseResolvedExtensionsOptions as UseResolvedExtensionsOptionsSDK } from '@openshift/dynamic-plugin-sdk'; +import { useResolvedExtensions as useResolvedExtensionsSDK } from '@openshift/dynamic-plugin-sdk'; import { useExtensions } from '@console/plugin-sdk/src/api/useExtensions'; -import { resolveExtension } from '../coderefs/coderef-resolver'; import type { UseResolvedExtensions } from '../extensions/console-types'; -import type { Extension, ExtensionTypeGuard, ResolvedExtension } from '../types'; -import { settleAllPromises } from '../utils/promise'; +import type { Extension, ExtensionPredicate, ResolvedExtension } from '../types'; + +const hookOptions: UseResolvedExtensionsOptionsSDK = { + useExtensionsImpl: useExtensions, +}; export const useResolvedExtensions: UseResolvedExtensions = ( - ...typeGuards: ExtensionTypeGuard[] + predicate: ExtensionPredicate, ): [ResolvedExtension[], boolean, any[]] => { - const extensions = useExtensions(...typeGuards); - - const [resolvedExtensions, setResolvedExtensions] = useState[]>([]); - const [resolved, setResolved] = useState(false); - const [errors, setErrors] = useState([]); - - useEffect(() => { - let disposed = false; - - // eslint-disable-next-line promise/catch-or-return - settleAllPromises( - extensions.map((e) => resolveExtension>(e)), - ).then(([fulfilledValues, rejectedReasons]) => { - if (!disposed) { - setResolvedExtensions(fulfilledValues); - setErrors(rejectedReasons); - setResolved(true); - - if (rejectedReasons.length > 0) { - // eslint-disable-next-line no-console - console.error('Detected errors while resolving Console extensions', rejectedReasons); - } - } - }); - - return () => { - disposed = true; - }; - }, [extensions]); - - return [resolvedExtensions, resolved, errors]; + return useResolvedExtensionsSDK(predicate, hookOptions); }; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/coderefs/__tests__/coderef-resolver.spec.ts b/frontend/packages/console-dynamic-plugin-sdk/src/coderefs/__tests__/coderef-resolver.spec.ts deleted file mode 100644 index a69ca986075..00000000000 --- a/frontend/packages/console-dynamic-plugin-sdk/src/coderefs/__tests__/coderef-resolver.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { isEncodedCodeRef, parseEncodedCodeRefValue } from '../coderef-resolver'; - -const originalConsole = { ...console }; -const consoleMock = jest.fn(); - -beforeEach(() => { - jest.resetAllMocks(); - // eslint-disable-next-line no-console - ['log', 'info', 'warn', 'error'].forEach((key) => (console[key] = consoleMock)); -}); - -afterEach(() => { - // eslint-disable-next-line no-console - ['log', 'info', 'warn', 'error'].forEach((key) => (console[key] = originalConsole[key])); -}); - -describe('isEncodedCodeRef', () => { - it('returns true if obj is structured as { $codeRef: string }', () => { - expect(isEncodedCodeRef({})).toBe(false); - expect(isEncodedCodeRef({ $codeRef: true })).toBe(false); - expect(isEncodedCodeRef({ $codeRef: 'foo' })).toBe(true); - expect(isEncodedCodeRef({ $codeRef: 'foo', bar: true })).toBe(false); - }); -}); - -describe('parseEncodedCodeRefValue', () => { - it('returns [moduleName, exportName] tuple if value has the right format', () => { - expect(parseEncodedCodeRefValue('foo.bar')).toEqual(['foo', 'bar']); - expect(parseEncodedCodeRefValue('foo')).toEqual(['foo', 'default']); - }); - - it('returns an empty array if value does not have the expected format', () => { - expect(parseEncodedCodeRefValue('')).toEqual([]); - expect(parseEncodedCodeRefValue('.')).toEqual([]); - expect(parseEncodedCodeRefValue('.bar')).toEqual([]); - expect(parseEncodedCodeRefValue('.bar.')).toEqual([]); - }); -}); diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/coderefs/coderef-resolver.ts b/frontend/packages/console-dynamic-plugin-sdk/src/coderefs/coderef-resolver.ts deleted file mode 100644 index fc71c324aaf..00000000000 --- a/frontend/packages/console-dynamic-plugin-sdk/src/coderefs/coderef-resolver.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* eslint-disable no-console */ - -import type { - AnyObject, - ExtractExtensionProperties, - ResolvedExtension, -} from '@openshift/dynamic-plugin-sdk'; -import { applyCodeRefSymbol } from '@openshift/dynamic-plugin-sdk'; -import { isPlainObject, isEqual, isFunction, isNil, cloneDeep } from 'lodash'; -import type { Extension, EncodedCodeRef, CodeRef } from '../types'; -import { deepForOwn } from '../utils/object'; -import { settleAllPromises } from '../utils/promise'; - -/** - * Extract the SDK's internal CodeRef symbol by applying it to a dummy function. - * - * This ensures we can detect code refs created by the SDK, which uses its own - * private Symbol instance. - */ -const codeRefSymbol = Object.getOwnPropertySymbols(applyCodeRefSymbol(() => Promise.resolve()))[0]; - -if (!codeRefSymbol) { - throw new Error('Failed to extract CodeRef symbol from the SDK'); -} - -export const isEncodedCodeRef = (obj): obj is EncodedCodeRef => - isPlainObject(obj) && - isEqual(Object.getOwnPropertyNames(obj), ['$codeRef']) && - typeof (obj as EncodedCodeRef).$codeRef === 'string'; - -export const isExecutableCodeRef = (obj): obj is CodeRef => - isFunction(obj) && - isEqual(Object.getOwnPropertySymbols(obj), [codeRefSymbol]) && - obj[codeRefSymbol] === true; - -const codeRefErrorSymbol = Symbol('error'); -export const isCodeRefError = (ref: CodeRef) => !!ref[codeRefErrorSymbol]; -export const getCodeRefError = (ref: CodeRef) => ref[codeRefErrorSymbol]; -export const setCodeRefError = (ref: CodeRef, e: any) => { - ref[codeRefErrorSymbol] = e; - return ref; -}; - -/** - * Parse the `EncodedCodeRef` value into `[moduleName, exportName]` tuple. - * - * Returns an empty array if the value doesn't match the expected format. - */ -export const parseEncodedCodeRefValue = (value: string): [string, string] | [] => { - const match = value.match(/^([^.]+)(?:\.(.+)){0,1}$/); - return match ? [match[1], match[2] || 'default'] : []; -}; - -/** - * Returns an extension with its `CodeRef` properties replaced with referenced objects. - * - * The resulting Promise resolves with a new extension instance; its `properties` object - * is cloned in order to preserve the original extension. - */ -export const resolveExtension = async < - E extends Extension, - P extends AnyObject = ExtractExtensionProperties, - R = ResolvedExtension ->( - extension: E, -): Promise => { - const clonedProperties = cloneDeep(extension.properties); - const valueResolutions: Promise[] = []; - - deepForOwn(clonedProperties, isExecutableCodeRef, (ref, key, obj) => { - if (isCodeRefError(ref)) { - throw getCodeRefError(ref); - } - valueResolutions.push( - ref() - .then((resolvedValue) => { - obj[key] = resolvedValue; - - if (isNil(resolvedValue)) { - console.warn(`Code reference property '${key}' resolved to null or undefined`); - } - }) - .catch((e) => { - setCodeRefError(ref, e ?? true); - return e; - }), - ); - }); - - await settleAllPromises(valueResolutions); - - // Return a new extension object with the resolved properties - return ({ - ...extension, - properties: clonedProperties, - } as unknown) as R; -}; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts b/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts index a1783812eeb..c60843669fa 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts @@ -29,7 +29,7 @@ import type { ResolvedExtension, Selector, } from '../api/common-types'; -import type { Extension, ExtensionTypeGuard } from '../types'; +import type { Extension, ExtensionPredicate } from '../types'; import type { CustomDataSource } from './dashboard-data-source'; export type OwnerReference = { @@ -264,7 +264,7 @@ export type UseK8sWatchResources = ( ) => WatchK8sResults; export type UseResolvedExtensions = ( - ...typeGuards: ExtensionTypeGuard[] + predicate: ExtensionPredicate, ) => [ResolvedExtension[], boolean, any[]]; export type GetSegmentAnalytics = () => { diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/types.ts b/frontend/packages/console-dynamic-plugin-sdk/src/types.ts index 050451352a3..ba5fbfd542d 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/types.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/types.ts @@ -3,7 +3,7 @@ export type { EncodedCodeRef, Extension, ExtensionFlags, - ExtensionPredicate as ExtensionTypeGuard, + ExtensionPredicate, LoadedAndResolvedExtension as ResolvedExtension, LoadedExtension, MapCodeRefsToValues as ResolvedCodeRefProperties, diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/validation/ExtensionValidator.ts b/frontend/packages/console-dynamic-plugin-sdk/src/validation/ExtensionValidator.ts index 067bf384f91..d7423cf2c95 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/validation/ExtensionValidator.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/validation/ExtensionValidator.ts @@ -1,16 +1,18 @@ import * as fs from 'fs'; import * as path from 'path'; +import { isEncodedCodeRef, parseEncodedCodeRef } from '@openshift/dynamic-plugin-sdk'; import * as _ from 'lodash'; import type { Compilation, NormalModule, Module } from 'webpack'; import type { ConsolePluginBuildMetadata } from '../build-types'; -import { isEncodedCodeRef, parseEncodedCodeRefValue } from '../coderefs/coderef-resolver'; import type { Extension, EncodedCodeRef } from '../types'; import { deepForOwn } from '../utils/object'; import { BaseValidator } from './BaseValidator'; type ExtensionCodeRefData = { index: number; - propToCodeRefValue: { [propName: string]: string }; + propToEncodedCodeRef: { + [propName: string]: EncodedCodeRef; + }; }; /** @@ -61,13 +63,13 @@ export const guessModuleFilePath = ( export const collectCodeRefData = (extensions: Extension[]) => extensions.reduce((acc, e, index) => { - const data: ExtensionCodeRefData = { index, propToCodeRefValue: {} }; + const data: ExtensionCodeRefData = { index, propToEncodedCodeRef: {} }; deepForOwn(e.properties, isEncodedCodeRef, (ref, key) => { - data.propToCodeRefValue[key] = ref.$codeRef; + data.propToEncodedCodeRef[key] = ref; }); - if (!_.isEmpty(data.propToCodeRefValue)) { + if (!_.isEmpty(data.propToEncodedCodeRef)) { acc.push(data); } @@ -116,9 +118,9 @@ export class ExtensionValidator extends BaseValidator { // Each exposed module must have at least one code reference Object.keys(exposedModules).forEach((moduleName) => { const moduleReferenced = codeRefs.some((data) => - Object.values(data.propToCodeRefValue).some((value) => { - const [parsedModuleName] = parseEncodedCodeRefValue(value); - return parsedModuleName && moduleName === parsedModuleName; + Object.values(data.propToEncodedCodeRef).some((ref) => { + const refData = parseEncodedCodeRef(ref); + return refData && moduleName === refData[0]; }), ); @@ -129,15 +131,16 @@ export class ExtensionValidator extends BaseValidator { // Each code reference must point to a valid webpack module export codeRefs.forEach((data) => { - Object.entries(data.propToCodeRefValue).forEach(([propName, codeRefValue]) => { - const [moduleName, exportName] = parseEncodedCodeRefValue(codeRefValue); + Object.entries(data.propToEncodedCodeRef).forEach(([propName, ref]) => { const errorTrace = `in extension [${data.index}] property '${propName}'`; + const refData = parseEncodedCodeRef(ref); - if (!moduleName || !exportName) { - this.result.addError(`Invalid code reference '${codeRefValue}' ${errorTrace}`); + if (!refData) { + this.result.addError(`Invalid code reference '${ref.$codeRef}' ${errorTrace}`); return; } + const [moduleName, exportName] = refData; const foundModule = webpackModules[moduleName]; if (!foundModule) { diff --git a/frontend/packages/console-plugin-sdk/src/api/useExtensions.ts b/frontend/packages/console-plugin-sdk/src/api/useExtensions.ts index 7e8d50523b0..61d04b44bc9 100644 --- a/frontend/packages/console-plugin-sdk/src/api/useExtensions.ts +++ b/frontend/packages/console-plugin-sdk/src/api/useExtensions.ts @@ -10,12 +10,13 @@ import { useTranslatedExtensions } from '../utils/useTranslatedExtensions'; * * This hook re-renders the component whenever the list of matching extensions changes. * - * The hook's result is guaranteed to be referentially stable across re-renders. + * The hook's result is guaranteed to be referentially stable across re-renders, + * assuming referential stability of the `predicate` parameter. * * @example * ```ts * const Example = () => { - * const navItemExtensions = useExtensions(isNavItem); + * const navItems = useExtensions(isNavItem); * // process extensions and render your component * }; * ``` @@ -25,7 +26,6 @@ import { useTranslatedExtensions } from '../utils/useTranslatedExtensions'; * @see {@link useTranslatedExtensions} * @see {@link useSortedExtensions} */ -// TODO: consider exposing hook via Console plugin SDK and move ^^ doc to exported symbol export const useExtensions: typeof useExtensionsSDK = (predicate) => { const extensions = useExtensionsSDK(predicate); const sortedExtensions = useSortedExtensions(extensions); diff --git a/frontend/packages/console-plugin-sdk/src/codegen/local-plugins.ts b/frontend/packages/console-plugin-sdk/src/codegen/local-plugins.ts index fb4bde2cbfc..5dfb631cd81 100644 --- a/frontend/packages/console-plugin-sdk/src/codegen/local-plugins.ts +++ b/frontend/packages/console-plugin-sdk/src/codegen/local-plugins.ts @@ -1,10 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; +import { isEncodedCodeRef, parseEncodedCodeRef } from '@openshift/dynamic-plugin-sdk'; import * as _ from 'lodash'; -import { - isEncodedCodeRef, - parseEncodedCodeRefValue, -} from '@console/dynamic-plugin-sdk/src/coderefs/coderef-resolver'; import { extensionsFile } from '@console/dynamic-plugin-sdk/src/constants'; import type { ConsoleExtensionsJSON } from '@console/dynamic-plugin-sdk/src/schema/console-extensions'; import type { EncodedCodeRef } from '@console/dynamic-plugin-sdk/src/types'; @@ -90,17 +87,18 @@ export const getExecutableCodeRefSource = ( pkg: PluginPackage, validationResult: ValidationResult, ) => { - const [moduleName, exportName] = parseEncodedCodeRefValue(ref.$codeRef); - const exposedModules = pkg.consolePlugin.exposedModules || {}; - const errorTrace = `in property '${propName}'`; const emptyCodeRefSource = '() => Promise.resolve(null)'; + const refData = parseEncodedCodeRef(ref); - if (!moduleName || !exportName) { + if (!refData) { validationResult.addError(`Invalid code reference '${ref.$codeRef}' ${errorTrace}`); return emptyCodeRefSource; } + const [moduleName, exportName] = refData; + const exposedModules = pkg.consolePlugin.exposedModules || {}; + if (!exposedModules[moduleName]) { validationResult.addError(`Module '${moduleName}' is not exposed ${errorTrace}`); return emptyCodeRefSource; diff --git a/frontend/packages/console-shared/src/components/catalog/hooks/__tests__/useCatalogExtensions.spec.ts b/frontend/packages/console-shared/src/components/catalog/hooks/__tests__/useCatalogExtensions.spec.ts index 11f060cc163..fa2c2d16aa6 100644 --- a/frontend/packages/console-shared/src/components/catalog/hooks/__tests__/useCatalogExtensions.spec.ts +++ b/frontend/packages/console-shared/src/components/catalog/hooks/__tests__/useCatalogExtensions.spec.ts @@ -17,7 +17,7 @@ let mockExtensions: ( )[] = []; jest.mock('@console/dynamic-plugin-sdk/src/api/useResolvedExtensions', () => ({ - useResolvedExtensions: (typeGuard) => [mockExtensions.filter(typeGuard), true], + useResolvedExtensions: (predicate) => [mockExtensions.filter(predicate), true], })); describe('useCatalogExtensions', () => { diff --git a/frontend/packages/console-shared/src/hooks/create-resource-hook.ts b/frontend/packages/console-shared/src/hooks/create-resource-hook.ts index 00e736e1772..60413cdc8e4 100644 --- a/frontend/packages/console-shared/src/hooks/create-resource-hook.ts +++ b/frontend/packages/console-shared/src/hooks/create-resource-hook.ts @@ -8,11 +8,11 @@ import { useExtensions } from '@console/plugin-sdk/src/api/useExtensions'; export const useCreateResourceExtension = ( modelReference: GroupVersionKind, ): LoadedExtension => { - const createResourceTypeGuard = useCallback( + const createResourcePredicate = useCallback( (e: Extension): e is CreateResource => isCreateResource(e) && referenceForExtensionModel(e.properties.model) === modelReference, [modelReference], ); - const [extensionPage] = useExtensions(createResourceTypeGuard); + const [extensionPage] = useExtensions(createResourcePredicate); return extensionPage; }; diff --git a/frontend/packages/console-shared/src/hooks/useDetailsItemExtensionsForResource.ts b/frontend/packages/console-shared/src/hooks/useDetailsItemExtensionsForResource.ts index 9870fac47d2..35ff44a867e 100644 --- a/frontend/packages/console-shared/src/hooks/useDetailsItemExtensionsForResource.ts +++ b/frontend/packages/console-shared/src/hooks/useDetailsItemExtensionsForResource.ts @@ -6,7 +6,7 @@ import type { DetailsItemColumn, } from '@console/dynamic-plugin-sdk/src/extensions/details-item'; import { isDetailsItem } from '@console/dynamic-plugin-sdk/src/extensions/details-item'; -import type { ResolvedExtension, ExtensionTypeGuard } from '@console/dynamic-plugin-sdk/src/types'; +import type { ResolvedExtension, ExtensionPredicate } from '@console/dynamic-plugin-sdk/src/types'; import { referenceFor, referenceForExtensionModel } from '@console/internal/module/k8s/k8s'; /** @@ -20,7 +20,7 @@ export const useDetailsItemExtensionsForResource: UseDetailsItemExtensionsForRes obj, column, ) => { - const typeGuard = useCallback>( + const predicate = useCallback>( (e): e is DetailsItem => { if (!isDetailsItem(e)) return false; const columnMatches = e.properties.column === column; @@ -30,7 +30,7 @@ export const useDetailsItemExtensionsForResource: UseDetailsItemExtensionsForRes [obj, column], ); - const [extensions] = useResolvedExtensions(typeGuard); + const [extensions] = useResolvedExtensions(predicate); return useMemo( () => diff --git a/frontend/yarn.lock b/frontend/yarn.lock index f8cbfb7632a..6919e92cf10 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3380,9 +3380,9 @@ __metadata: languageName: node linkType: hard -"@openshift/dynamic-plugin-sdk@npm:^8.0.0": - version: 8.0.0 - resolution: "@openshift/dynamic-plugin-sdk@npm:8.0.0" +"@openshift/dynamic-plugin-sdk@file:../../dynamic-plugin-sdk/packages/lib-core::locator=openshift-console%40workspace%3A.": + version: 8.1.0 + resolution: "@openshift/dynamic-plugin-sdk@file:../../dynamic-plugin-sdk/packages/lib-core#../../dynamic-plugin-sdk/packages/lib-core::hash=07ab8b&locator=openshift-console%40workspace%3A." dependencies: lodash: "npm:^4.17.23" semver: "npm:^7.7.3" @@ -3390,7 +3390,7 @@ __metadata: yup: "npm:^1.7.1" peerDependencies: react: ^18 || ^19 - checksum: 10c0/a5c7bc5198f75fcc9fad242ffb51e264e860218e7035b87204d429cc47fa566208cc3474af739d3e05a2adc1bfe41ae9e09d3afd93a5b8c0d29f1814ee9f3127 + checksum: 10c0/019198a5a5be99d8dd17557e19601a340672a8914c87c3d7672be626cb8751a69992f27ba41a47791a1309bbb722684d16c5356b89f15d580a2b24a49dc3f9de languageName: node linkType: hard @@ -17801,7 +17801,7 @@ __metadata: "@graphql-codegen/typescript": "npm:^1.15.1" "@graphql-codegen/typescript-graphql-files-modules": "npm:^1.15.1" "@graphql-codegen/typescript-operations": "npm:^1.15.1" - "@openshift/dynamic-plugin-sdk": "npm:^8.0.0" + "@openshift/dynamic-plugin-sdk": "file:../../dynamic-plugin-sdk/packages/lib-core" "@openshift/dynamic-plugin-sdk-webpack": "npm:^5.1.0" "@patternfly/patternfly": "npm:~6.4.0" "@patternfly/quickstarts": "npm:~6.4.0" From 01d3e80d64c12ed6186f574f398df7ce36bdf689 Mon Sep 17 00:00:00 2001 From: logonoff Date: Tue, 10 Mar 2026 10:14:23 -0400 Subject: [PATCH 2/3] CONSOLE-5065: Remove ModelFeatureFlag any cast --- frontend/public/reducers/features.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/frontend/public/reducers/features.ts b/frontend/public/reducers/features.ts index fdfe4cb1765..62ffa6157b1 100644 --- a/frontend/public/reducers/features.ts +++ b/frontend/public/reducers/features.ts @@ -19,10 +19,11 @@ import { MachineModel, PrometheusModel, } from '../models'; -import { K8sModel } from '../module/k8s'; -import { referenceForGroupVersionKind, referenceForModel } from '../module/k8s/k8s-ref'; +import { K8sModel, referenceForExtensionModel } from '../module/k8s'; +import { referenceForModel } from '../module/k8s/k8s-ref'; import type { RootState } from '../redux'; import { ActionType as K8sActionType } from '@console/dynamic-plugin-sdk/src/app/k8s/actions/k8s'; +import type { LoadedExtension } from '@openshift/dynamic-plugin-sdk'; import { FeatureState } from '@console/dynamic-plugin-sdk/src/app/features'; import { FeatureAction, ActionType } from '../actions/flags'; import { pluginStore } from '../plugins'; @@ -67,17 +68,14 @@ const addToCRDs = (ref: string, flag: string) => { } }; -const getModelRef = (e: ModelFeatureFlag) => { - const model = e.properties.model; - return referenceForGroupVersionKind(model.group)(model.version)(model.kind); -}; +const getModelRef = (e: ModelFeatureFlag) => referenceForExtensionModel(e.properties.model); -// TODO: When migrating to @openshift/dynamic-plugin-sdk, use the type parameter from -// pluginStore.getExtensions<...>() to avoid `as any` cast. -(pluginStore.getExtensions().filter(isModelFeatureFlag) as any).forEach((ff) => { - // This is incorrect (for `ExtensionK8sModel` we should use `referenceForExtensionModel`). - addToCRDs(referenceForModel(ff.properties.model), ff.properties.flag); -}); +pluginStore + .getExtensions() + .filter(isModelFeatureFlag) + .forEach((ff: LoadedExtension) => { + addToCRDs(getModelRef(ff), ff.properties.flag); + }); export const featureReducerName = 'FLAGS'; export const featureReducer = (state: FeatureState, action: FeatureAction): FeatureState => { From 19f647100cd6ed7bec40f2a0d687a8d2fc5775f4 Mon Sep 17 00:00:00 2001 From: logonoff Date: Fri, 6 Mar 2026 16:34:17 -0500 Subject: [PATCH 3/3] TODO: Use new version --- dynamic-demo-plugin/yarn.lock | 12 ++++++------ frontend/.yarnrc.yml | 2 ++ frontend/package.json | 2 +- frontend/yarn.lock | 8 ++++---- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/dynamic-demo-plugin/yarn.lock b/dynamic-demo-plugin/yarn.lock index 4edcad8cd3b..bdbfb2cb2a4 100644 --- a/dynamic-demo-plugin/yarn.lock +++ b/dynamic-demo-plugin/yarn.lock @@ -153,7 +153,7 @@ __metadata: version: 0.0.0-use.local resolution: "@openshift-console/dynamic-plugin-sdk-webpack@portal:../frontend/packages/console-dynamic-plugin-sdk/dist/webpack::locator=%40console%2Fdynamic-demo-plugin%40workspace%3A." dependencies: - "@openshift/dynamic-plugin-sdk": "npm:^8.0.0" + "@openshift/dynamic-plugin-sdk": "git+https://github.com/openshift/dynamic-plugin-sdk.git#workspace=@openshift/dynamic-plugin-sdk&head=main" "@openshift/dynamic-plugin-sdk-webpack": "npm:^5.1.0" ajv: "npm:^6.12.3" chalk: "npm:2.4.x" @@ -173,7 +173,7 @@ __metadata: version: 0.0.0-use.local resolution: "@openshift-console/dynamic-plugin-sdk@portal:../frontend/packages/console-dynamic-plugin-sdk/dist/core::locator=%40console%2Fdynamic-demo-plugin%40workspace%3A." dependencies: - "@openshift/dynamic-plugin-sdk": "npm:^8.0.0" + "@openshift/dynamic-plugin-sdk": "git+https://github.com/openshift/dynamic-plugin-sdk.git#workspace=@openshift/dynamic-plugin-sdk&head=main" immutable: "npm:3.x" lodash: "npm:^4.17.21" reselect: "npm:4.x" @@ -223,9 +223,9 @@ __metadata: languageName: node linkType: hard -"@openshift/dynamic-plugin-sdk@npm:^8.0.0": - version: 8.0.0 - resolution: "@openshift/dynamic-plugin-sdk@npm:8.0.0" +"@openshift/dynamic-plugin-sdk@git+https://github.com/openshift/dynamic-plugin-sdk.git#workspace=@openshift/dynamic-plugin-sdk&head=main": + version: 8.1.0 + resolution: "@openshift/dynamic-plugin-sdk@https://github.com/openshift/dynamic-plugin-sdk.git#workspace=%40openshift%2Fdynamic-plugin-sdk&commit=c35748e717d3cb259c10c5f70831bbf7705ef457" dependencies: lodash: "npm:^4.17.23" semver: "npm:^7.7.3" @@ -233,7 +233,7 @@ __metadata: yup: "npm:^1.7.1" peerDependencies: react: ^18 || ^19 - checksum: 10c0/a5c7bc5198f75fcc9fad242ffb51e264e860218e7035b87204d429cc47fa566208cc3474af739d3e05a2adc1bfe41ae9e09d3afd93a5b8c0d29f1814ee9f3127 + checksum: 10c0/57d3fa90221884b613e6dedf69208219b5697be69352ef24ba71dd8404a40aeca29d39dc3896d6734ad1289b645705db45879183a1e02bcd45d4cfee7190f5bb languageName: node linkType: hard diff --git a/frontend/.yarnrc.yml b/frontend/.yarnrc.yml index 4feef5fc40a..9ab990e38b9 100644 --- a/frontend/.yarnrc.yml +++ b/frontend/.yarnrc.yml @@ -1,3 +1,5 @@ +checksumBehavior: ignore + enableInlineBuilds: true enableScripts: false diff --git a/frontend/package.json b/frontend/package.json index 78bdd928f39..9ddcf698db6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -149,7 +149,7 @@ ] }, "dependencies": { - "@openshift/dynamic-plugin-sdk": "file:../../dynamic-plugin-sdk/packages/lib-core", + "@openshift/dynamic-plugin-sdk": "git+https://github.com/openshift/dynamic-plugin-sdk.git#workspace=@openshift/dynamic-plugin-sdk&head=main", "@openshift/dynamic-plugin-sdk-webpack": "^5.1.0", "@patternfly/patternfly": "~6.4.0", "@patternfly/quickstarts": "~6.4.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 6919e92cf10..5b48c9cb6fd 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3380,9 +3380,9 @@ __metadata: languageName: node linkType: hard -"@openshift/dynamic-plugin-sdk@file:../../dynamic-plugin-sdk/packages/lib-core::locator=openshift-console%40workspace%3A.": +"@openshift/dynamic-plugin-sdk@git+https://github.com/openshift/dynamic-plugin-sdk.git#workspace=@openshift/dynamic-plugin-sdk&head=main": version: 8.1.0 - resolution: "@openshift/dynamic-plugin-sdk@file:../../dynamic-plugin-sdk/packages/lib-core#../../dynamic-plugin-sdk/packages/lib-core::hash=07ab8b&locator=openshift-console%40workspace%3A." + resolution: "@openshift/dynamic-plugin-sdk@https://github.com/openshift/dynamic-plugin-sdk.git#workspace=%40openshift%2Fdynamic-plugin-sdk&commit=c35748e717d3cb259c10c5f70831bbf7705ef457" dependencies: lodash: "npm:^4.17.23" semver: "npm:^7.7.3" @@ -3390,7 +3390,7 @@ __metadata: yup: "npm:^1.7.1" peerDependencies: react: ^18 || ^19 - checksum: 10c0/019198a5a5be99d8dd17557e19601a340672a8914c87c3d7672be626cb8751a69992f27ba41a47791a1309bbb722684d16c5356b89f15d580a2b24a49dc3f9de + checksum: 10c0/57d3fa90221884b613e6dedf69208219b5697be69352ef24ba71dd8404a40aeca29d39dc3896d6734ad1289b645705db45879183a1e02bcd45d4cfee7190f5bb languageName: node linkType: hard @@ -17801,7 +17801,7 @@ __metadata: "@graphql-codegen/typescript": "npm:^1.15.1" "@graphql-codegen/typescript-graphql-files-modules": "npm:^1.15.1" "@graphql-codegen/typescript-operations": "npm:^1.15.1" - "@openshift/dynamic-plugin-sdk": "file:../../dynamic-plugin-sdk/packages/lib-core" + "@openshift/dynamic-plugin-sdk": "git+https://github.com/openshift/dynamic-plugin-sdk.git#workspace=@openshift/dynamic-plugin-sdk&head=main" "@openshift/dynamic-plugin-sdk-webpack": "npm:^5.1.0" "@patternfly/patternfly": "npm:~6.4.0" "@patternfly/quickstarts": "npm:~6.4.0"