diff --git a/dynamic-demo-plugin/yarn.lock b/dynamic-demo-plugin/yarn.lock index e9bd9411bbe..441dfd57a07 100644 --- a/dynamic-demo-plugin/yarn.lock +++ b/dynamic-demo-plugin/yarn.lock @@ -462,7 +462,6 @@ __metadata: dependencies: "@openshift/api-types": "npm:^1.0.0" "@openshift/dynamic-plugin-sdk": "npm:^9.1.0" - immutable: "npm:^3.8.3" lodash: "npm:^4.18.1" reselect: "npm:^5.1.1" typesafe-actions: "npm:^5.1.0" @@ -2381,13 +2380,6 @@ __metadata: languageName: node linkType: hard -"immutable@npm:^3.8.3": - version: 3.8.3 - resolution: "immutable@npm:3.8.3" - checksum: 10c0/bafa7b8371b7622bc3d128cd9e6bba3a654b968f09a237929629f43ac26f7e974a5879cd38baad0c26f6f0628753968611bf832add7bf0c44d647bf4306a2988 - languageName: node - linkType: hard - "import-local@npm:^3.0.2": version: 3.2.0 resolution: "import-local@npm:3.2.0" diff --git a/frontend/package.json b/frontend/package.json index 8e1ac56c6c7..852b2818c75 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,7 @@ "check-cycles": "CHECK_CYCLES=true yarn dev-once", "coverage": "jest --coverage .", "eslint": "node ./node_modules/.bin/eslint --max-warnings ${MAX_WARNINGS:-0} --color", - "lint": "NODE_OPTIONS=--max-old-space-size=4096 MAX_WARNINGS=343 yarn eslint --format ./scripts/eslint-exact-warnings.js .", + "lint": "NODE_OPTIONS=--max-old-space-size=4096 MAX_WARNINGS=342 yarn eslint --format ./scripts/eslint-exact-warnings.js .", "gherkin-lint": "./node_modules/.bin/gherkin-lint -c ./packages/dev-console/integration-tests/.gherkin-lintrc ./packages/*/integration-tests/features", "test": "LANG=en_US.UTF-8 jest", "debug-test": "node --inspect-brk node_modules/.bin/jest --runInBand", @@ -97,7 +97,6 @@ "i18next-conv": "16.0.0", "i18next-http-backend": "^4.0.1", "i18next-v4-format-converter": "^1.1.2", - "immutable": "^3.8.3", "istextorbinary": "^9.5.0", "js-base64": "^3.9.2", "js-yaml": "^3.15.0", diff --git a/frontend/packages/console-app/src/__tests__/extension-checks/yaml-templates.spec.ts b/frontend/packages/console-app/src/__tests__/extension-checks/yaml-templates.spec.ts index e8d98ec70fd..1e9c20d157a 100644 --- a/frontend/packages/console-app/src/__tests__/extension-checks/yaml-templates.spec.ts +++ b/frontend/packages/console-app/src/__tests__/extension-checks/yaml-templates.spec.ts @@ -1,4 +1,3 @@ -import type { Map as ImmutableMap } from 'immutable'; import * as _ from 'lodash'; import type { YAMLTemplate } from '@console/dynamic-plugin-sdk/src/extensions/yaml-templates'; import { isYAMLTemplate } from '@console/dynamic-plugin-sdk/src/extensions/yaml-templates'; @@ -8,20 +7,11 @@ import { referenceForExtensionModel } from '@console/internal/module/k8s'; import { useExtensions } from '@console/plugin-sdk/src/api/useExtensions'; import { renderHookWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; -type TemplateEntry = [GroupVersionKind, ImmutableMap]; +type TemplateEntry = [GroupVersionKind, Record]; -const entryToKeys = (entry: TemplateEntry) => { - const keys: string[] = []; - - entry[1] - .keySeq() - .toArray() - .forEach((templateName) => { - keys.push(`${entry[0]}_${templateName}`); // e.g. 'apps~v1~ReplicaSet_default' - }); - - return keys; -}; +// e.g. 'apps~v1~ReplicaSet_default' +const entryToKeys = (entry: TemplateEntry) => + Object.keys(entry[1]).map((templateName) => `${entry[0]}_${templateName}`); const extensionToKeys = (e: YAMLTemplate) => [ `${referenceForExtensionModel(e.properties.model)}_${e.properties.name || 'default'}`, @@ -33,7 +23,7 @@ describe('YAMLTemplate', () => { it('only one named template per model is allowed', async () => { const { result } = await renderHookWithProviders(() => useExtensions(isYAMLTemplate)); - const baseTemplateEntries = _.values(baseTemplates.entrySeq().toObject()) as TemplateEntry[]; + const baseTemplateEntries = Object.entries(baseTemplates) as TemplateEntry[]; const baseTemplateKeys = _.flatMap(baseTemplateEntries.map(entryToKeys)); const pluginTemplateKeys = _.flatMap( result.current.filter(isYAMLTemplate).map(extensionToKeys), diff --git a/frontend/packages/console-app/src/components/admission-webhook-warnings/AdmissionWebhookWarningNotifications.tsx b/frontend/packages/console-app/src/components/admission-webhook-warnings/AdmissionWebhookWarningNotifications.tsx index e962d6b5291..0eafb4ccee3 100644 --- a/frontend/packages/console-app/src/components/admission-webhook-warnings/AdmissionWebhookWarningNotifications.tsx +++ b/frontend/packages/console-app/src/components/admission-webhook-warnings/AdmissionWebhookWarningNotifications.tsx @@ -1,6 +1,5 @@ import { useEffect } from 'react'; import { AlertVariant } from '@patternfly/react-core'; -import type { Map as ImmutableMap } from 'immutable'; import { useTranslation } from 'react-i18next'; import { getAdmissionWebhookWarnings, @@ -15,9 +14,9 @@ import { useToast } from '@console/shared/src/components/toast/useToast'; import { useConsoleDispatch } from '@console/shared/src/hooks/useConsoleDispatch'; import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector'; -type UseAdmissionWebhookWarnings = () => ImmutableMap; +type UseAdmissionWebhookWarnings = () => Record; const useAdmissionWebhookWarnings: UseAdmissionWebhookWarnings = () => - useConsoleSelector>(getAdmissionWebhookWarnings); + useConsoleSelector>(getAdmissionWebhookWarnings); export const AdmissionWebhookWarningNotifications = () => { const { t } = useTranslation('console-app'); @@ -26,7 +25,7 @@ export const AdmissionWebhookWarningNotifications = () => { const admissionWebhookWarnings = useAdmissionWebhookWarnings(); useEffect(() => { const docURL = getDocumentationURL(documentationURLs.admissionWebhookWarning); - admissionWebhookWarnings.forEach((warning, id) => { + Object.entries(admissionWebhookWarnings).forEach(([id, warning]) => { toastContext.addToast({ variant: AlertVariant.warning, title: t('Admission Webhook Warning'), diff --git a/frontend/packages/console-app/src/components/console-operator/ConsolePluginCSPStatusDetail.tsx b/frontend/packages/console-app/src/components/console-operator/ConsolePluginCSPStatusDetail.tsx index 99550d17c2e..5ace6fc9b74 100644 --- a/frontend/packages/console-app/src/components/console-operator/ConsolePluginCSPStatusDetail.tsx +++ b/frontend/packages/console-app/src/components/console-operator/ConsolePluginCSPStatusDetail.tsx @@ -7,9 +7,7 @@ import { ConsolePluginCSPStatus } from './ConsolePluginStatus'; const ConsolePluginCSPStatusDetail: FC = ({ obj }) => { const pluginName = useMemo(() => obj?.metadata?.name, [obj?.metadata?.name]); - const cspViolations = useConsoleSelector(({ UI }) => - UI.get('pluginCSPViolations'), - ); + const cspViolations = useConsoleSelector(({ UI }) => UI.pluginCSPViolations); return ; }; diff --git a/frontend/packages/console-app/src/components/console-operator/ConsolePluginsTable.tsx b/frontend/packages/console-app/src/components/console-operator/ConsolePluginsTable.tsx index daa035820b9..add8deeaa69 100644 --- a/frontend/packages/console-app/src/components/console-operator/ConsolePluginsTable.tsx +++ b/frontend/packages/console-app/src/components/console-operator/ConsolePluginsTable.tsx @@ -411,9 +411,7 @@ const ConsolePluginsTable: FC = ({ const DevPluginsPage: FC = (props) => { const pluginInfo = usePluginInfo(); - const cspViolations = useConsoleSelector(({ UI }) => - UI.get('pluginCSPViolations'), - ); + const cspViolations = useConsoleSelector(({ UI }) => UI.pluginCSPViolations); const rows = useMemo( () => @@ -439,9 +437,7 @@ const useConsolePluginRows = (enabledPlugins: string[]) => { isList: true, kind: referenceForModel(ConsolePluginModel), }); - const cspViolations = useConsoleSelector(({ UI }) => - UI.get('pluginCSPViolations'), - ); + const cspViolations = useConsoleSelector(({ UI }) => UI.pluginCSPViolations); const rows = useMemo(() => { if (!consolePluginsLoaded) { diff --git a/frontend/packages/console-app/src/components/console-operator/__tests__/ConsolePluginCSPStatusDetail.spec.tsx b/frontend/packages/console-app/src/components/console-operator/__tests__/ConsolePluginCSPStatusDetail.spec.tsx index a1eafe0b009..dc0ba61c918 100644 --- a/frontend/packages/console-app/src/components/console-operator/__tests__/ConsolePluginCSPStatusDetail.spec.tsx +++ b/frontend/packages/console-app/src/components/console-operator/__tests__/ConsolePluginCSPStatusDetail.spec.tsx @@ -1,5 +1,4 @@ import { screen } from '@testing-library/react'; -import { Map as ImmutableMap } from 'immutable'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; import ConsolePluginCSPStatusDetail from '../ConsolePluginCSPStatusDetail'; @@ -17,9 +16,9 @@ describe('ConsolePluginCSPStatusDetail', () => { const renderWithCSPState = (pluginName: string, cspViolations: Record) => { renderWithProviders(, { initialState: { - UI: ImmutableMap({ + UI: { pluginCSPViolations: cspViolations, - }), + }, }, }); }; diff --git a/frontend/packages/console-app/src/components/dashboards-page/dynamic-plugins-health-resource/DynamicPluginsPopover.tsx b/frontend/packages/console-app/src/components/dashboards-page/dynamic-plugins-health-resource/DynamicPluginsPopover.tsx index c439b5f3f57..1ca365ee249 100644 --- a/frontend/packages/console-app/src/components/dashboards-page/dynamic-plugins-health-resource/DynamicPluginsPopover.tsx +++ b/frontend/packages/console-app/src/components/dashboards-page/dynamic-plugins-health-resource/DynamicPluginsPopover.tsx @@ -15,9 +15,7 @@ import NotLoadedDynamicPlugins from './NotLoadedDynamicPlugins'; const DynamicPluginsPopover: FC = ({ consolePlugins }) => { const { t } = useTranslation('console-app'); const pluginInfoEntries = usePluginInfo(); - const cspViolations = useConsoleSelector(({ UI }) => - UI.get('pluginCSPViolations'), - ); + const cspViolations = useConsoleSelector(({ UI }) => UI.pluginCSPViolations); const notLoadedDynamicPluginInfo = pluginInfoEntries.filter( (plugin) => plugin.status !== 'loaded', ); diff --git a/frontend/packages/console-app/src/components/flags/FeatureFlagExtensionLoader.tsx b/frontend/packages/console-app/src/components/flags/FeatureFlagExtensionLoader.tsx index e24a08b7a44..898e2d5c139 100644 --- a/frontend/packages/console-app/src/components/flags/FeatureFlagExtensionLoader.tsx +++ b/frontend/packages/console-app/src/components/flags/FeatureFlagExtensionLoader.tsx @@ -97,7 +97,7 @@ const useModelFeatureFlagExtensions = () => { const [resolvedExtensions] = useResolvedExtensions(isModelFeatureFlag); const dispatch = useConsoleDispatch(); - const models = useConsoleSelector(({ k8s }) => k8s.getIn(['RESOURCES', 'models'])); + const models = useConsoleSelector(({ k8s }) => k8s.RESOURCES?.models); // Use a ref to always access the current models value without changing the callback identity const modelsRef = useRef(models); @@ -109,7 +109,7 @@ const useModelFeatureFlagExtensions = () => { (added, removed) => { // The feature reducer can't access state from the k8s reducer, so get the // models here and include them in the action payload. - dispatch(updateModelFlags(added, removed, modelsRef.current)); + dispatch(updateModelFlags(added, removed, Object.values(modelsRef.current ?? {}))); }, [dispatch], ); diff --git a/frontend/packages/console-app/src/components/flags/__tests__/FeatureFlagExtensionLoader.spec.tsx b/frontend/packages/console-app/src/components/flags/__tests__/FeatureFlagExtensionLoader.spec.tsx index c5d60d4c379..1f607f05476 100644 --- a/frontend/packages/console-app/src/components/flags/__tests__/FeatureFlagExtensionLoader.spec.tsx +++ b/frontend/packages/console-app/src/components/flags/__tests__/FeatureFlagExtensionLoader.spec.tsx @@ -5,11 +5,6 @@ import { renderHookWithProviders } from '@console/shared/src/test-utils/unit-tes import { createTestPluginStore } from '../../console-operator/__tests__/pluginTestUtils'; import { useFeatureFlagController } from '../FeatureFlagExtensionLoader'; -const renderController = () => - renderHookWithProviders(() => useFeatureFlagController(), { - pluginStore: createTestPluginStore(), - }); - describe('useFeatureFlagController', () => { it('defers flag updates made during render until after the render completes', async () => { let flagDuringRender: boolean | undefined; @@ -19,7 +14,7 @@ describe('useFeatureFlagController', () => { const setFeatureFlag = useFeatureFlagController(); // Simulate console.flag/hookProvider handlers that set flags during render. setFeatureFlag('SYNC_FLAG', true); - flagDuringRender = reduxStore.getState().FLAGS.get('SYNC_FLAG'); + flagDuringRender = reduxStore.getState().FLAGS.SYNC_FLAG; return setFeatureFlag; }, { pluginStore: createTestPluginStore() }, @@ -32,22 +27,26 @@ describe('useFeatureFlagController', () => { await Promise.resolve(); }); - expect(store.getState().FLAGS.get('SYNC_FLAG')).toBe(true); + expect(store.getState().FLAGS.SYNC_FLAG).toBe(true); }); it('applies async flag updates without waiting for another render', async () => { - const { store, result } = renderController(); + const { store, result } = renderHookWithProviders(() => useFeatureFlagController(), { + pluginStore: createTestPluginStore(), + }); await act(async () => { result.current('ASYNC_FLAG', true); await Promise.resolve(); }); - expect(store.getState().FLAGS.get('ASYNC_FLAG')).toBe(true); + expect(store.getState().FLAGS.ASYNC_FLAG).toBe(true); }); it('coalesces consecutive async updates to the latest value', async () => { - const { store, result } = renderController(); + const { store, result } = renderHookWithProviders(() => useFeatureFlagController(), { + pluginStore: createTestPluginStore(), + }); await act(async () => { result.current('TOGGLE_FLAG', true); @@ -55,15 +54,17 @@ describe('useFeatureFlagController', () => { await Promise.resolve(); }); - expect(store.getState().FLAGS.get('TOGGLE_FLAG')).toBe(false); + expect(store.getState().FLAGS.TOGGLE_FLAG).toBe(false); }); it('preserves flag updates made reentrantly during flush', async () => { - const { store, result } = renderController(); + const { store, result } = renderHookWithProviders(() => useFeatureFlagController(), { + pluginStore: createTestPluginStore(), + }); await act(async () => { const unsubscribe = store.subscribe(() => { - if (store.getState().FLAGS.get('REENTRANT_FLAG') === true) { + if (store.getState().FLAGS.REENTRANT_FLAG === true) { result.current('REENTRANT_FLAG', false); unsubscribe(); } @@ -73,6 +74,6 @@ describe('useFeatureFlagController', () => { await Promise.resolve(); }); - expect(store.getState().FLAGS.get('REENTRANT_FLAG')).toBe(false); + expect(store.getState().FLAGS.REENTRANT_FLAG).toBe(false); }); }); diff --git a/frontend/packages/console-app/src/components/nodes/NodesPage.tsx b/frontend/packages/console-app/src/components/nodes/NodesPage.tsx index d4f30ba65f9..cf61e915706 100644 --- a/frontend/packages/console-app/src/components/nodes/NodesPage.tsx +++ b/frontend/packages/console-app/src/components/nodes/NodesPage.tsx @@ -697,7 +697,7 @@ const NodeList: FC = ({ }) => { const { t } = useTranslation('console-app'); const { columns, resetAllColumnWidths } = useNodesColumns(vmsEnabled, isOpenShift5); - const nodeMetrics = useConsoleSelector(({ UI }) => UI.getIn(['metrics', 'node'])); + const nodeMetrics = useConsoleSelector(({ UI }) => UI.metrics?.node); const columnManagementID = referenceForModel(NodeModel); const statusExtensions = useNodeStatusExtensions(); diff --git a/frontend/packages/console-app/src/hooks/useCSPViolationDetector.tsx b/frontend/packages/console-app/src/hooks/useCSPViolationDetector.tsx index 449f3d8e02d..673f8557424 100644 --- a/frontend/packages/console-app/src/hooks/useCSPViolationDetector.tsx +++ b/frontend/packages/console-app/src/hooks/useCSPViolationDetector.tsx @@ -87,9 +87,7 @@ export const useCSPViolationDetector = () => { const toastContext = useToast(); const fireTelemetryEvent = useTelemetry(); const pluginStore = usePluginStore(); - const cspViolations = useConsoleSelector(({ UI }) => - UI.get('pluginCSPViolations'), - ); + const cspViolations = useConsoleSelector(({ UI }) => UI.pluginCSPViolations); const dispatch = useConsoleDispatch(); const [, cacheEvent] = useLocalStorageCache( LOCAL_STORAGE_CSP_VIOLATIONS_KEY, diff --git a/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md b/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md index b4529de7a2a..724338c23f9 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md +++ b/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md @@ -10,6 +10,10 @@ For current development version of Console, use `4.x.0-prerelease.n` packages. For older 1.x plugin SDK packages, refer to "OpenShift Console Versions vs SDK Versions" compatibility table in [Console dynamic plugins README](./README.md). +## 5.1.0-prerelease.1 - TBD + +- Removed `immutable` dependency from the redux store and from the package ([CONSOLE-5001], [#17024]) + ## 4.23.0-prerelease.6 - TBD - Add an `onCancel` prop to `ResourceYAMLEditor` to allow overriding the default cancel behavior ([CONSOLE-5438], [#16941]) @@ -245,6 +249,7 @@ table in [Console dynamic plugins README](./README.md). [CONSOLE-4951]: https://issues.redhat.com/browse/CONSOLE-4951 [CONSOLE-4954]: https://issues.redhat.com/browse/CONSOLE-4954 [CONSOLE-4990]: https://issues.redhat.com/browse/CONSOLE-4990 +[CONSOLE-5001]: https://issues.redhat.com/browse/CONSOLE-5001 [CONSOLE-5039]: https://issues.redhat.com/browse/CONSOLE-5039 [CONSOLE-5050]: https://issues.redhat.com/browse/CONSOLE-5050 [CONSOLE-5063]: https://issues.redhat.com/browse/CONSOLE-5063 @@ -355,3 +360,4 @@ table in [Console dynamic plugins README](./README.md). [#16750]: https://github.com/openshift/console/pull/16750 [#16762]: https://github.com/openshift/console/pull/16762 [#16941]: https://github.com/openshift/console/pull/16941 +[#17024]: https://github.com/openshift/console/pull/17024 diff --git a/frontend/packages/console-dynamic-plugin-sdk/release-notes/5.1.md b/frontend/packages/console-dynamic-plugin-sdk/release-notes/5.1.md new file mode 100644 index 00000000000..a4892c56502 --- /dev/null +++ b/frontend/packages/console-dynamic-plugin-sdk/release-notes/5.1.md @@ -0,0 +1,12 @@ +# OpenShift Console 5.1 Release Notes + +## Changes to the Redux store + +> [!NOTE] +> Plugins must not access or read Console-owned Redux state directly. Console exposes the Redux store only so +> that plugins can create and manage their own section of the store. + +The Console-owned slices of the Redux store (such as `core`, `features`, `dashboards`, `UI`, `observe`, and `k8s`) +no longer use [Immutable.js](https://immutable-js.com/). These state slices are plain JavaScript objects instead +of `Immutable.Map` and `Immutable.List` instances. Because these slices are plain JavaScript objects, Immutable.js +APIs such as `.get()` and `.getIn()` no longer work on them. diff --git a/frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts b/frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts index b5193ed1741..90732ab0dcb 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts @@ -130,13 +130,7 @@ export const getCorePackage: GetPackageDefinition = ( dependencies: { ...parseDeps( rootPackage, - [ - '@openshift/api-types', - '@openshift/dynamic-plugin-sdk', - 'immutable', - 'reselect', - 'typesafe-actions', - ], + ['@openshift/api-types', '@openshift/dynamic-plugin-sdk', 'reselect', 'typesafe-actions'], missingDepCallback, ), ...parseDepsAs(rootPackage, { 'lodash-es': 'lodash' }, missingDepCallback), @@ -170,7 +164,7 @@ export const getInternalPackage: GetPackageDefinition = ( main: 'lib/lib-internal.js', ...commonManifestFields, dependencies: { - ...parseDeps(rootPackage, ['@openshift/dynamic-plugin-sdk', 'immutable'], missingDepCallback), + ...parseDeps(rootPackage, ['@openshift/dynamic-plugin-sdk'], missingDepCallback), }, }, filesToCopy: { diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/api/internal-types.ts b/frontend/packages/console-dynamic-plugin-sdk/src/api/internal-types.ts index 6fb2cd9e9d1..f3b62b590f4 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/api/internal-types.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/api/internal-types.ts @@ -3,7 +3,6 @@ import type { QuickStart } from '@patternfly/quickstarts'; import type { OverflowMenuProps } from '@patternfly/react-core'; import type { DataViewTh } from '@patternfly/react-data-view/dist/esm/DataViewTable/DataViewTable'; import type { SortByDirection } from '@patternfly/react-table'; -import type { Map as ImmutableMap } from 'immutable'; import type { HealthState, K8sResourceCommon, @@ -242,14 +241,14 @@ export enum ActionMenuVariant { } type Request = { - active: boolean; - timeout: NodeJS.Timer; - inFlight: boolean; - data: R; - error: any; + active?: number; + timeout?: ReturnType; + inFlight?: boolean; + data?: R; + loadError?: any; }; -export type RequestMap = ImmutableMap>; +export type RequestMap = Record>; export type Fetch = (url: string) => Promise; export type WatchURLProps = { diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/__tests__/core.spec.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/__tests__/core.spec.ts index 11597388d71..0ae7369cb9e 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/__tests__/core.spec.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/__tests__/core.spec.ts @@ -1,5 +1,4 @@ -import { Map as ImmutableMap } from 'immutable'; -import type { AdmissionWebhookWarning, CoreState } from '../../../redux-types'; +import type { CoreState } from '../../../redux-types'; import { setUser, beginImpersonate, endImpersonate } from '../../actions/core'; import { coreReducer } from '../core'; import reducerTest from './utils/reducerTest'; @@ -7,9 +6,9 @@ import reducerTest from './utils/reducerTest'; describe('Core Reducer', () => { const state: CoreState = { user: {}, - admissionWebhookWarnings: ImmutableMap(), + admissionWebhookWarnings: {}, }; - const mockAdmissionWebhookWarnings = ImmutableMap({}); + const mockAdmissionWebhookWarnings = {}; it('set user', () => { const mockUser = { diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/core.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/core.ts index 5b311c6031c..6bc7700eab5 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/core.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/core.ts @@ -1,5 +1,4 @@ -import { Map as ImmutableMap } from 'immutable'; -import type { AdmissionWebhookWarning, CoreState } from '../../redux-types'; +import type { CoreState } from '../../redux-types'; import type { CoreAction } from '../actions/core'; import { ActionType } from '../actions/core'; @@ -16,7 +15,7 @@ export const coreReducer = ( state: CoreState = { user: {}, userResource: null, - admissionWebhookWarnings: ImmutableMap(), + admissionWebhookWarnings: {}, }, action: CoreAction = undefined, ): CoreState => { @@ -59,16 +58,18 @@ export const coreReducer = ( case ActionType.SetAdmissionWebhookWarning: return { ...state, - admissionWebhookWarnings: state.admissionWebhookWarnings.set( - action.payload.id, - action.payload.warning, - ), + admissionWebhookWarnings: { + ...state.admissionWebhookWarnings, + [action.payload.id]: action.payload.warning, + }, }; - case ActionType.RemoveAdmissionWebhookWarning: + case ActionType.RemoveAdmissionWebhookWarning: { + const { [action.payload.id]: _, ...remaining } = state.admissionWebhookWarnings; return { ...state, - admissionWebhookWarnings: state.admissionWebhookWarnings.remove(action.payload.id), + admissionWebhookWarnings: remaining, }; + } default: return state; } diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts index 466025d72e2..ea8bc479825 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts @@ -1,4 +1,3 @@ -import type { Map as ImmutableMap } from 'immutable'; import type { UserInfo, UserKind } from '../../../extensions'; import type { ImpersonateKind, SDKStoreState, AdmissionWebhookWarning } from '../../redux-types'; @@ -7,7 +6,7 @@ type GetUser = (state: SDKStoreState) => UserInfo; type GetUserResource = (state: SDKStoreState) => UserKind; type GetAdmissionWebhookWarnings = ( state: SDKStoreState, -) => ImmutableMap; +) => Record; /** * It provides impersonation details from the redux store. diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/features.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/features.ts index 35cc71ef462..d7798a28e45 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/features.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/features.ts @@ -1,6 +1,4 @@ -import type { Map as ImmutableMap } from 'immutable'; - -export type FeatureState = ImmutableMap; +export type FeatureState = Record; export type FeatureSubStore = { FLAGS: FeatureState; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/k8s/reducers/k8s.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/k8s/reducers/k8s.ts index 8aebb8ded1d..8ddf466b319 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/k8s/reducers/k8s.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/k8s/reducers/k8s.ts @@ -1,4 +1,3 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; import * as _ from 'lodash'; import type { K8sModel } from '../../../api/common-types'; import { getReferenceForModel } from '../../../utils/k8s/k8s-ref'; @@ -10,9 +9,6 @@ import { getK8sDataById } from './k8sSelector'; const getQN: (obj) => string = (obj) => { const { name, namespace } = obj.metadata; - // Name + namespace is not unique for PackageManifest resources, so include the catalog source. - // TODO: We should be able to remove this when the upstream OLM bug is fixed: - // https://bugzilla.redhat.com/show_bug.cgi?id=1814822 if (obj.apiVersion === 'packages.operators.coreos.com/v1' && obj.kind === 'PackageManifest') { return `(${obj.status?.catalogSource})-${name}`; } @@ -20,196 +16,213 @@ const getQN: (obj) => string = (obj) => { }; const moreRecent = (a, b) => { - const metaA = a.get('metadata').toJSON(); - const metaB = b.get('metadata').toJSON(); + const metaA = a.metadata; + const metaB = b.metadata; if (metaA.uid !== metaB.uid) { return new Date(metaA.creationTimestamp) > new Date(metaB.creationTimestamp); } return parseInt(metaA.resourceVersion, 10) > parseInt(metaB.resourceVersion, 10); }; -const removeFromList = (list, resource) => { +const removeFromList = (list: Record, resource) => { const qualifiedName = getQN(resource); // eslint-disable-next-line no-console console.log(`deleting ${qualifiedName}`); - return list.delete(qualifiedName); + const { [qualifiedName]: _removed, ...remaining } = list; + return remaining; }; -const updateList = (list: ImmutableMap, nextJS) => { - const qualifiedName = getQN(nextJS); - const current = list.get(qualifiedName); - const next = fromJS(nextJS); +const updateList = (list: Record, nextObj) => { + const qualifiedName = getQN(nextObj); + const current = list[qualifiedName]; if (!current) { - return list.set(qualifiedName, next); + return { ...list, [qualifiedName]: nextObj }; } - if (!moreRecent(next, current)) { + if (!moreRecent(nextObj, current)) { return list; } - // TODO: (kans) only store the data for things we display ... - // and then only do this comparison for the same stuff! - if ( - current - .deleteIn(['metadata', 'resourceVersion']) - .equals(next.deleteIn(['metadata', 'resourceVersion'])) - ) { - // If the only thing that differs is resource version, don't fire an update. + const currentNoRV = { + ...current, + metadata: { ...current.metadata, resourceVersion: undefined }, + }; + const nextNoRV = { + ...nextObj, + metadata: { ...nextObj.metadata, resourceVersion: undefined }, + }; + if (_.isEqual(currentNoRV, nextNoRV)) { return list; } - return list.set(qualifiedName, next); + return { ...list, [qualifiedName]: nextObj }; }; -const loadList = (oldList, resources) => { - const existingKeys = new Set(oldList.keys()); - return oldList.withMutations((list) => { - (resources || []).forEach((r) => { - const qualifiedName = getQN(r); - existingKeys.delete(qualifiedName); - const next = fromJS(r); - const current = list.get(qualifiedName); - if (!current || moreRecent(next, current)) { - list.set(qualifiedName, next); - } - }); - existingKeys.forEach((k) => { - const r = list.get(k); - const metadata = r.get('metadata').toJSON(); - if (!metadata.deletionTimestamp) { - // eslint-disable-next-line no-console - console.warn(`${metadata.namespace}-${metadata.name} is gone with no deletion timestamp!`); - } - list.delete(k); - }); +const loadList = (oldList: Record, resources) => { + const newList = { ...oldList }; + const existingKeys = new Set(Object.keys(newList)); + + (resources || []).forEach((r) => { + const qualifiedName = getQN(r); + existingKeys.delete(qualifiedName); + const current = newList[qualifiedName]; + if (!current || moreRecent(r, current)) { + newList[qualifiedName] = r; + } + }); + + existingKeys.forEach((k) => { + const r = newList[k]; + const { metadata } = r; + if (!metadata.deletionTimestamp) { + // eslint-disable-next-line no-console + console.warn(`${metadata.namespace}-${metadata.name} is gone with no deletion timestamp!`); + } + delete newList[k]; }); + + return newList; }; const sdkK8sReducers = (state: K8sState, action: K8sAction): K8sState => { if (!state) { - return fromJS({ + return { RESOURCES: { - // Loaded k8s models (CRDs), might also be empty on a cluster without any CRD! - models: ImmutableMap(), - // Indicates whether a loading is 'in flight' (in progress), could jump back and forth. + models: {} as Record, inFlight: false, - // Indicates whether a some data was ever loaded successfully, changes just once to true. loaded: false, }, - }); + }; } let newList; switch (action.type) { case ActionType.GetResourcesInFlight: - return state.setIn(['RESOURCES', 'inFlight'], true); + return { + ...state, + RESOURCES: { ...state.RESOURCES, inFlight: true }, + }; + + case ActionType.ReceivedResources: { + const currentModels = state.RESOURCES.models; + const updatedModels = { ...currentModels }; - case ActionType.ReceivedResources: - return ( - action.payload.resources.models - .filter((model) => !state.getIn(['RESOURCES', 'models']).has(getReferenceForModel(model))) - .filter((model) => { - const existingModel = state.getIn(['RESOURCES', 'models', model.kind]); - return ( - !existingModel || getReferenceForModel(existingModel) !== getReferenceForModel(model) - ); - }) - .map((model) => { - model.namespaced - ? getNamespacedResources().add(getReferenceForModel(model)) - : getNamespacedResources().delete(getReferenceForModel(model)); - return model; - }) - .reduce((prevState, newModel) => { - // FIXME: Need to use `kind` as model reference for legacy components accessing k8s primitives - const [modelRef, model] = allModels().findEntry( - (staticModel) => getReferenceForModel(staticModel) === getReferenceForModel(newModel), - ) || [getReferenceForModel(newModel), newModel]; - // Verbs and short names are not part of the static model definitions, so use the values found during discovery. - return prevState.updateIn(['RESOURCES', 'models'], (models) => - models.set(modelRef, { - ...model, - verbs: newModel.verbs, - shortNames: newModel.shortNames, - }), - ); - }, state) - // TODO: Determine where these are used and implement filtering in that component instead of storing in Redux - .setIn(['RESOURCES', 'allResources'], action.payload.resources.allResources) - .setIn(['RESOURCES', 'safeResources'], action.payload.resources.safeResources) - .setIn(['RESOURCES', 'adminResources'], action.payload.resources.adminResources) - .setIn(['RESOURCES', 'configResources'], action.payload.resources.configResources) - .setIn( - ['RESOURCES', 'clusterOperatorConfigResources'], - action.payload.resources.clusterOperatorConfigResources, - ) - .setIn(['RESOURCES', 'namespacedSet'], action.payload.resources.namespacedSet) - .setIn(['RESOURCES', 'groupToVersionMap'], action.payload.resources.groupVersionMap) - .setIn(['RESOURCES', 'inFlight'], false) - .setIn(['RESOURCES', 'loaded'], true) - ); + action.payload.resources.models + .filter((model) => !currentModels[getReferenceForModel(model)]) + .filter((model) => { + const existingModel = currentModels[model.kind]; + return ( + !existingModel || getReferenceForModel(existingModel) !== getReferenceForModel(model) + ); + }) + .forEach((newModel) => { + newModel.namespaced + ? getNamespacedResources().add(getReferenceForModel(newModel)) + : getNamespacedResources().delete(getReferenceForModel(newModel)); + + const entry = Object.entries(allModels()).find( + ([, staticModel]) => + getReferenceForModel(staticModel as K8sModel) === getReferenceForModel(newModel), + ); + const [modelRef, model] = entry || [getReferenceForModel(newModel), newModel]; + updatedModels[modelRef] = { + ...(model as K8sModel), + verbs: newModel.verbs, + shortNames: newModel.shortNames, + }; + }); + + return { + ...state, + RESOURCES: { + ...state.RESOURCES, + models: updatedModels, + allResources: action.payload.resources.allResources, + safeResources: action.payload.resources.safeResources, + adminResources: action.payload.resources.adminResources, + configResources: action.payload.resources.configResources, + clusterOperatorConfigResources: action.payload.resources.clusterOperatorConfigResources, + namespacedSet: action.payload.resources.namespacedSet, + groupToVersionMap: action.payload.resources.groupVersionMap, + inFlight: false, + loaded: true, + }, + }; + } case ActionType.StartWatchK8sObject: - return state.set( - action.payload.id, - ImmutableMap({ + return { + ...state, + [action.payload.id]: { loadError: '', loaded: false, data: {}, - }), - ); + }, + }; case ActionType.StartWatchK8sList: if (getK8sDataById(state, action.payload.id)) { return state; } - // We mergeDeep instead of overwriting state because it's possible to add filters before load/watching - return state.mergeDeep({ + return { + ...state, [action.payload.id]: { + ...state[action.payload.id], loadError: '', - // has the data set been loaded successfully loaded: false, - // Canonical data - data: ImmutableMap(), - // client side filters to be applied externally (ie, we keep all data intact) - filters: ImmutableMap(), - // The name of an element in the list that has been "selected" + data: {}, + filters: {}, selected: null, }, - }); + }; case ActionType.ModifyObject: { const { k8sObjects, id } = action.payload; - let currentJS = getK8sDataById(state, id) || {}; - // getIn can return JS object or Immutable object - if (currentJS.toJSON) { - currentJS = currentJS.toJSON(); - currentJS.metadata.resourceVersion = k8sObjects.metadata.resourceVersion; - if (_.isEqual(currentJS, k8sObjects)) { - // If the only thing that differs is resource version, don't fire an update. + const currentData = getK8sDataById(state, id) || {}; + if ( + currentData.metadata && + currentData.metadata.resourceVersion !== k8sObjects.metadata.resourceVersion + ) { + const currentNoRV = { + ...currentData, + metadata: { + ...currentData.metadata, + resourceVersion: k8sObjects.metadata.resourceVersion, + }, + }; + if (_.isEqual(currentNoRV, k8sObjects)) { return state; } } - return state.mergeIn([id], { - loadError: '', - loaded: true, - data: k8sObjects, - }); + return { + ...state, + [id]: { + ...state[id], + loadError: '', + loaded: true, + data: k8sObjects, + }, + }; } - case ActionType.StopWatchK8s: - return state.delete(action.payload.id); + case ActionType.StopWatchK8s: { + const { [action.payload.id]: _removed, ...remaining } = state; + return remaining; + } case ActionType.Errored: if (!getK8sDataById(state, action.payload.id)) { return state; } - /* Don't overwrite data or loaded state if there was an error. Better to - * keep stale data around than to suddenly have it disappear on a user. - */ - return state.setIn([action.payload.id, 'loadError'], action.payload.k8sObjects); + return { + ...state, + [action.payload.id]: { + ...state[action.payload.id], + loadError: action.payload.k8sObjects, + }, + }; case ActionType.Loaded: if (!getK8sDataById(state, action.payload.id)) { @@ -217,16 +230,19 @@ const sdkK8sReducers = (state: K8sState, action: K8sAction): K8sState => { } // eslint-disable-next-line no-console console.info(`loaded ${action.payload.id}`); - // eslint-disable-next-line no-param-reassign - state = state.mergeDeep({ - [action.payload.id]: { loaded: true, loadError: '' }, - }); newList = loadList(getK8sDataById(state, action.payload.id), action.payload.k8sObjects); - break; + return { + ...state, + [action.payload.id]: { + ...state[action.payload.id], + loaded: true, + loadError: '', + data: newList, + }, + }; case ActionType.UpdateListFromWS: newList = getK8sDataById(state, action.payload.id); - // k8sObjects is an array of k8s WS Events for (const { type, object } of action.payload.k8sObjects) { switch (type) { case 'DELETED': @@ -237,32 +253,49 @@ const sdkK8sReducers = (state: K8sState, action: K8sAction): K8sState => { newList = updateList(newList, object); break; default: - // possible `ERROR` type or other // eslint-disable-next-line no-console console.warn(`unknown websocket action: ${type}`); - // console.warn(`unknown websocket action: ${type} (${_.get(event, 'object.message')})`); } } - break; + return { + ...state, + [action.payload.id]: { + ...state[action.payload.id], + data: newList, + }, + }; case ActionType.BulkAddToList: if (!getK8sDataById(state, action.payload.id)) { return state; } - newList = getK8sDataById(state, action.payload.id); - newList = newList.merge( - action.payload.k8sObjects.reduce( - (map, obj) => map.set(getQN(obj), fromJS(obj)), - ImmutableMap(), - ), - ); - break; + newList = { ...getK8sDataById(state, action.payload.id) }; + action.payload.k8sObjects.forEach((obj) => { + newList[getQN(obj)] = obj; + }); + return { + ...state, + [action.payload.id]: { + ...state[action.payload.id], + data: newList, + }, + }; + case ActionType.FilterList: - return state.setIn([action.payload.id, 'filters', action.payload.name], action.payload.value); + return { + ...state, + [action.payload.id]: { + ...state[action.payload.id], + filters: { + ...state[action.payload.id]?.filters, + [action.payload.name]: action.payload.value, + }, + }, + }; + default: return state; } - return state.setIn([action.payload.id, 'data'], newList); }; export default sdkK8sReducers; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/k8s/reducers/k8sSelector.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/k8s/reducers/k8sSelector.ts index b879baa6730..73fc50f140f 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/k8s/reducers/k8sSelector.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/k8s/reducers/k8sSelector.ts @@ -1,5 +1,5 @@ import type { K8sState } from '../../redux-types'; -export const getReduxIdPayload = (state, reduxId) => state.k8s.get(reduxId); +export const getReduxIdPayload = (state, reduxId) => state.k8s[reduxId]; -export const getK8sDataById = (state: K8sState, id: string) => state.getIn([id, 'data']); +export const getK8sDataById = (state: K8sState, id: string) => state[id]?.data; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts index 977cd42d6f2..5e720824766 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts @@ -1,9 +1,8 @@ -import type { Map as ImmutableMap } from 'immutable'; import type { AnyAction } from 'redux'; import type { ThunkDispatch } from 'redux-thunk'; import type { UserInfo, UserKind } from '../extensions/console-types'; -export type K8sState = ImmutableMap; +export type K8sState = Record; export type AdmissionWebhookWarning = { kind: string; @@ -21,7 +20,7 @@ export type CoreState = { user?: UserInfo; userResource?: UserKind; impersonate?: ImpersonateKind; - admissionWebhookWarnings?: ImmutableMap; + admissionWebhookWarnings?: Record; }; export type SDKStoreState = { diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/flags.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/flags.ts index 4190372b525..863b30b1764 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/utils/flags.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/flags.ts @@ -9,4 +9,4 @@ export type UseFlag = (flag: string) => boolean; * @returns the boolean value of the requested feature flag or undefined */ export const useFlag: UseFlag = (flag) => - useSelector(({ FLAGS }) => FLAGS.get(flag)); + useSelector(({ FLAGS }) => FLAGS[flag]); diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/__tests__/k8s-watcher.spec.tsx b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/__tests__/k8s-watcher.spec.tsx index 2cd666983f8..f92f9f83411 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/__tests__/k8s-watcher.spec.tsx +++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/__tests__/k8s-watcher.spec.tsx @@ -1,11 +1,3 @@ -import { - List as ImmutableList, - Stack as ImmutableStack, - Set as ImmutableSet, - OrderedSet as ImmutableOrderedSet, - Map as ImmutableMap, - OrderedMap as ImmutableOrderedMap, -} from 'immutable'; import type { WatchK8sResource } from '../../../../extensions/console-types'; import { getReduxData } from '../k8s-watcher'; @@ -16,137 +8,51 @@ describe('getReduxData', () => { expect(getReduxData(undefined, resource)).toBe(null); }); - it('should convert ImmutableList to pure JSON', () => { - const immutableData = ImmutableList([ - ImmutableMap({ a: 1 }), - ImmutableMap({ b: 2 }), - ImmutableMap({ c: 3 }), - ]); + it('should convert a Record to an array for isList: true', () => { + const data = { a: { a: 1 }, b: { b: 2 }, c: { c: 3 } }; const resource: WatchK8sResource = { isList: true }; - expect(getReduxData(immutableData, resource)).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]); + expect(getReduxData(data, resource)).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]); }); - it('should convert ImmutableStack to pure JSON', () => { - const immutableData = ImmutableStack([ - ImmutableMap({ a: 1 }), - ImmutableMap({ b: 2 }), - ImmutableMap({ c: 3 }), - ]); - const resource: WatchK8sResource = { isList: true }; - expect(getReduxData(immutableData, resource)).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]); - }); - - it('should convert ImmutableSet to pure JSON', () => { - const immutableData = ImmutableSet([ - ImmutableMap({ a: 1 }), - ImmutableMap({ b: 2 }), - ImmutableMap({ c: 3 }), - ]); - const resource: WatchK8sResource = { isList: true }; - expect(getReduxData(immutableData, resource)).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]); - }); - - it('should convert ImmutableOrderedSet to pure JSON', () => { - const immutableData = ImmutableOrderedSet([ - ImmutableMap({ a: 1 }), - ImmutableMap({ b: 2 }), - ImmutableMap({ c: 3 }), - ]); - const resource: WatchK8sResource = { isList: true }; - expect(getReduxData(immutableData, resource)).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]); - }); - - it('should convert ImmutableMap to pure JSON', () => { - const immutableData = ImmutableMap({ a: 1, b: 2, c: 3 }); + it('should return data directly for non-list resources', () => { + const data = { a: 1, b: 2, c: 3 }; const resource: WatchK8sResource = {}; - expect(getReduxData(immutableData, resource)).toEqual({ a: 1, b: 2, c: 3 }); + expect(getReduxData(data, resource)).toEqual({ a: 1, b: 2, c: 3 }); }); - it('should convert ImmutableOrderedMap to pure JSON', () => { - const immutableData = ImmutableOrderedMap({ a: 1, b: 2, c: 3 }); + it('should return the same reference for non-list data', () => { + const data = { a: 1, b: 2, c: 3 }; const resource: WatchK8sResource = {}; - expect(getReduxData(immutableData, resource)).toEqual({ a: 1, b: 2, c: 3 }); - }); - - it('should return the same JSON object for unchanged data', () => { - const immutableData = ImmutableMap({ a: 1, b: 2, c: 3 }); - const resource: WatchK8sResource = {}; - const firstTime = getReduxData(immutableData, resource); - const secondTime = getReduxData(immutableData, resource); - expect(firstTime).toEqual({ a: 1, b: 2, c: 3 }); - expect(secondTime).toEqual({ a: 1, b: 2, c: 3 }); - expect(firstTime).toBe(secondTime); - }); - - it('should return a new JSON object if the data has changed', () => { - const immutableData = ImmutableMap({ a: 1, b: 2, c: 3 }); - const changedData = immutableData.set('c', 4); - const resource: WatchK8sResource = {}; - const firstTime = getReduxData(immutableData, resource); - const secondTime = getReduxData(changedData, resource); - expect(firstTime).toEqual({ a: 1, b: 2, c: 3 }); - expect(secondTime).toEqual({ a: 1, b: 2, c: 4 }); - expect(firstTime).not.toBe(secondTime); - }); - - it('should return the same JSON array and child objects for unchanged data', () => { - const immutableData = ImmutableList([ - ImmutableMap({ a: 1 }), - ImmutableMap({ b: 2 }), - ImmutableMap({ c: 3 }), - ]); - const resource: WatchK8sResource = { isList: true }; - const firstTime = getReduxData(immutableData, resource); - const secondTime = getReduxData(immutableData, resource); - expect(firstTime).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]); - expect(secondTime).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]); - // The array instance should be the same + const firstTime = getReduxData(data, resource); + const secondTime = getReduxData(data, resource); expect(firstTime).toBe(secondTime); - expect(firstTime[0]).toBe(secondTime[0]); - expect(firstTime[1]).toBe(secondTime[1]); - expect(firstTime[2]).toBe(secondTime[2]); }); - it('should return a new JSON array but same unchanged child objects for changed data', () => { - const immutableData = ImmutableList([ - ImmutableMap({ a: 1 }), - ImmutableMap({ b: 2 }), - ImmutableMap({ c: 3 }), - ]); - const changedData = immutableData.setIn([2, 'c'], 4); + it('should preserve element references in list data', () => { + const item1 = { a: 1 }; + const item2 = { b: 2 }; + const item3 = { c: 3 }; + const data = { x: item1, y: item2, z: item3 }; const resource: WatchK8sResource = { isList: true }; - const firstTime = getReduxData(immutableData, resource); - const secondTime = getReduxData(changedData, resource); - expect(firstTime).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]); - expect(secondTime).toEqual([{ a: 1 }, { b: 2 }, { c: 4 }]); - // The array should be changed - expect(firstTime).not.toBe(secondTime); - // But the included object data should return the same instance - expect(firstTime[0]).toBe(secondTime[0]); - expect(firstTime[1]).toBe(secondTime[1]); - // Except for the changed object obviously - expect(firstTime[2]).not.toBe(secondTime[2]); + const result = getReduxData(data, resource); + expect(result).toContain(item1); + expect(result).toContain(item2); + expect(result).toContain(item3); }); - it('should return different data for isList true and false, but same data when calling multiple times', () => { - const immutableData = ImmutableMap({ - a: ImmutableMap({ a: 1 }), - b: ImmutableMap({ b: 2 }), - c: ImmutableMap({ c: 3 }), - }); - const listFirstTime = getReduxData(immutableData, { isList: true }); - const noListFirstTime = getReduxData(immutableData, { isList: false }); - const listSecondTime = getReduxData(immutableData, { isList: true }); - const noListSecondTime = getReduxData(immutableData, { isList: false }); + it('should return different results for isList true and false', () => { + const data = { a: { a: 1 }, b: { b: 2 }, c: { c: 3 } }; + const listResult = getReduxData(data, { isList: true }); + const noListResult = getReduxData(data, { isList: false }); - // Contains the right data - expect(listFirstTime).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]); - expect(noListFirstTime).toEqual({ a: { a: 1 }, b: { b: 2 }, c: { c: 3 } }); - expect(listSecondTime).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]); - expect(noListSecondTime).toEqual({ a: { a: 1 }, b: { b: 2 }, c: { c: 3 } }); + expect(listResult).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]); + expect(noListResult).toEqual({ a: { a: 1 }, b: { b: 2 }, c: { c: 3 } }); + }); - // Contains the same (cached) data for both calls - expect(listFirstTime).toBe(listSecondTime); - expect(noListFirstTime).toBe(noListSecondTime); + it('should return the input array as-is for list data that is already an array', () => { + const data = [{ a: 1 }, { b: 2 }, { c: 3 }]; + const resource: WatchK8sResource = { isList: true }; + const result = getReduxData(data, resource); + expect(result).toBe(data); }); }); diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/k8s-watcher.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/k8s-watcher.ts index 73373ca33b9..f14f04a0c3e 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/k8s-watcher.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/k8s-watcher.ts @@ -47,36 +47,33 @@ export const makeQuery: MakeQuery = (namespace, labelSelector, fieldSelector, na return query; }; -const INTERNAL_REDUX_IMMUTABLE_TOARRAY_CACHE_SYMBOL = Symbol('_cachedToArrayResult'); -const INTERNAL_REDUX_IMMUTABLE_TOJSON_CACHE_SYMBOL = Symbol('_cachedToJSONResult'); +// Cache the array derived from a keyed list object so repeated calls with the +// same stored reference return a stable array reference. This preserves +// referential stability, avoiding unnecessary consumer re-renders. +const reduxListDataCache = new WeakMap(); -export const getReduxData = (immutableData, resource: WatchK8sResource) => { - if (!immutableData) { +export const getReduxData = (data, resource: WatchK8sResource) => { + if (data == null) { return null; } - if (resource.isList && immutableData.toArray) { - if (!immutableData[INTERNAL_REDUX_IMMUTABLE_TOARRAY_CACHE_SYMBOL]) { - immutableData[INTERNAL_REDUX_IMMUTABLE_TOARRAY_CACHE_SYMBOL] = immutableData - .toArray() - .map((a) => { - if (a.toJSON) { - if (!a[INTERNAL_REDUX_IMMUTABLE_TOJSON_CACHE_SYMBOL]) { - a[INTERNAL_REDUX_IMMUTABLE_TOJSON_CACHE_SYMBOL] = a.toJSON(); - } - return a[INTERNAL_REDUX_IMMUTABLE_TOJSON_CACHE_SYMBOL]; - } - return a; - }); + if (resource.isList) { + if (Array.isArray(data) || typeof data !== 'object') { + return data; } - return immutableData[INTERNAL_REDUX_IMMUTABLE_TOARRAY_CACHE_SYMBOL]; - } - if (immutableData.toJSON) { - if (!immutableData[INTERNAL_REDUX_IMMUTABLE_TOJSON_CACHE_SYMBOL]) { - immutableData[INTERNAL_REDUX_IMMUTABLE_TOJSON_CACHE_SYMBOL] = immutableData.toJSON(); + let list = reduxListDataCache.get(data); + if (!list) { + list = Object.values(data); + reduxListDataCache.set(data, list); } - return immutableData[INTERNAL_REDUX_IMMUTABLE_TOJSON_CACHE_SYMBOL]; + return list; + } + // Before the watched object has loaded, the reducer stores an empty + // placeholder object. Surface that as null rather than an empty object, matching + // the previous behavior where only fully loaded objects produced data. + if (typeof data === 'object' && !Array.isArray(data) && Object.keys(data).length === 0) { + return null; } - return null; + return data; }; export const getIDAndDispatch: GetIDAndDispatch = (resource, k8sModel) => { diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sModel.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sModel.ts index 451ad6beda2..55e5a9f8bc4 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sModel.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sModel.ts @@ -14,8 +14,8 @@ export const getK8sModel = ( ): K8sModel => { const kindReference = transformGroupVersionKindToReference(k8sGroupVersionKind); return kindReference - ? (k8s.getIn(['RESOURCES', 'models', kindReference]) ?? - k8s.getIn(['RESOURCES', 'models', getGroupVersionKindForReference(kindReference).kind])) + ? (k8s.RESOURCES?.models?.[kindReference] ?? + k8s.RESOURCES?.models?.[getGroupVersionKindForReference(kindReference).kind]) : undefined; }; @@ -33,5 +33,5 @@ export const getK8sModel = ( */ export const useK8sModel: UseK8sModel = (k8sGroupVersionKind) => [ useSelector(({ k8s }) => getK8sModel(k8s, k8sGroupVersionKind)), - useSelector(({ k8s }) => k8s.getIn(['RESOURCES', 'inFlight']) ?? false), + useSelector(({ k8s }) => k8s.RESOURCES?.inFlight ?? false), ]; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sModels.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sModels.ts index 38fc99d8283..5551f0bd770 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sModels.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sModels.ts @@ -1,13 +1,9 @@ import { useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; import type { SDKStoreState } from '../../../app/redux-types'; import type { UseK8sModels } from '../../../extensions/console-types'; import type { K8sModel } from '../../../lib-core'; -const modelsSelector = createSelector( - (state: SDKStoreState) => state.k8s.getIn(['RESOURCES', 'models']), - (models) => models?.toJS() ?? {}, -); +const EMPTY_MODELS: { [key: string]: K8sModel } = {}; /** * Hook that retrieves all current k8s models from redux. @@ -22,6 +18,8 @@ const modelsSelector = createSelector( * ``` */ export const useK8sModels: UseK8sModels = () => [ - useSelector(modelsSelector), - useSelector(({ k8s }) => k8s.getIn(['RESOURCES', 'inFlight'])) ?? false, + useSelector( + ({ k8s }) => k8s.RESOURCES?.models ?? EMPTY_MODELS, + ), + useSelector(({ k8s }) => k8s.RESOURCES?.inFlight ?? false), ]; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResource.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResource.ts index d37bb7cdff7..c51da4e7e8d 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResource.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResource.ts @@ -1,5 +1,4 @@ import { useMemo, useEffect } from 'react'; -import type { Map as ImmutableMap } from 'immutable'; import { useDispatch, useSelector } from 'react-redux'; import * as k8sActions from '../../../app/k8s/actions/k8s'; import { getReduxIdPayload } from '../../../app/k8s/reducers/k8sSelector'; @@ -48,7 +47,7 @@ export const useK8sWatchResource: UseK8sWatchResource = (initResource) => { const resourceK8s = useSelector((state) => reduxID ? getReduxIdPayload(state, reduxID.id) : null, - ) as ImmutableMap; + ) as Record; return useMemo(() => { if (!resource) { @@ -61,9 +60,9 @@ export const useK8sWatchResource: UseK8sWatchResource = (initResource) => { : [data, false, undefined]; } - const data = getReduxData(resourceK8s.get('data'), resource); - const loaded = resourceK8s.get('loaded'); - const loadError = resourceK8s.get('loadError'); + const data = getReduxData(resourceK8s?.data, resource); + const loaded = resourceK8s?.loaded; + const loadError = resourceK8s?.loadError; return [data, loaded, loadError]; }, [resource, resourceK8s, modelsLoaded, k8sModel]); }; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResources.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResources.ts index 054ca00c4e2..2330a14a10a 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResources.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResources.ts @@ -1,6 +1,4 @@ import { useRef, useMemo, useEffect } from 'react'; -import type { Iterable as ImmutableIterable } from 'immutable'; -import { Map as ImmutableMap } from 'immutable'; import { useDispatch, useSelector } from 'react-redux'; import { createSelectorCreator, lruMemoize } from 'reselect'; import type { K8sModel } from '../../../api/common-types'; @@ -40,14 +38,15 @@ export const useK8sWatchResources: UseK8sWatchResources = (initResources) => { const resources = useDeepCompareMemoize(initResources, true); const modelsLoaded = useModelsLoaded(); - const allK8sModels = useSelector((state) => - state.k8s.getIn(['RESOURCES', 'models']), - ) as ImmutableMap; + const allK8sModels = useSelector((state) => state.k8s.RESOURCES?.models) as Record< + string, + K8sModel + >; const prevK8sModels = usePrevious(allK8sModels); const prevResources = usePrevious(resources); - const k8sModelsRef = useRef>(ImmutableMap()); + const k8sModelsRef = useRef>({}); if ( prevResources !== resources || @@ -62,9 +61,12 @@ export const useK8sWatchResources: UseK8sWatchResources = (initResources) => { const requiredModels = Object.values(resources).map((r) => transformGroupVersionKindToReference(r.groupVersionKind || r.kind), ); - k8sModelsRef.current = allK8sModels.filter( - (model) => - requiredModels.includes(getReferenceForModel(model)) || requiredModels.includes(model.kind), + k8sModelsRef.current = Object.fromEntries( + Object.entries(allK8sModels ?? {}).filter( + ([, model]) => + requiredModels.includes(getReferenceForModel(model)) || + requiredModels.includes(model.kind), + ), ); } @@ -82,8 +84,8 @@ export const useK8sWatchResources: UseK8sWatchResources = (initResources) => { const resourceModel = modelReference && - (k8sModels.get(modelReference) || - k8sModels.get(getGroupVersionKindForReference(modelReference).kind)); + (k8sModels[modelReference] || + k8sModels[getGroupVersionKindForReference(modelReference).kind]); if (!resourceModel) { ids[key] = { noModel: true, @@ -122,13 +124,10 @@ export const useK8sWatchResources: UseK8sWatchResources = (initResources) => { createSelectorCreator({ memoize: lruMemoize, memoizeOptions: { - equalityCheck: ( - oldK8s: ImmutableMap, - newK8s: ImmutableMap, - ) => + equalityCheck: (oldK8s: Record, newK8s: Record) => Object.keys(reduxIDs || {}) .filter((k) => !reduxIDs[k].noModel) - .every((k) => oldK8s.get(reduxIDs[k].id) === newK8s.get(reduxIDs[k].id)), + .every((k) => oldK8s[reduxIDs[k].id] === newK8s[reduxIDs[k].id]), }, }), [reduxIDs], @@ -154,10 +153,10 @@ export const useK8sWatchResources: UseK8sWatchResources = (initResources) => { loaded: true, loadError: new NoModelError(), }; - } else if (resourceK8s.has(reduxIDs?.[key].id)) { - const data = getReduxData(resourceK8s.getIn([reduxIDs[key].id, 'data']), resources[key]); - const loaded = resourceK8s.getIn([reduxIDs[key].id, 'loaded']); - const loadError = resourceK8s.getIn([reduxIDs[key].id, 'loadError']); + } else if (reduxIDs?.[key].id in (resourceK8s ?? {})) { + const data = getReduxData(resourceK8s[reduxIDs[key].id]?.data, resources[key]); + const loaded = resourceK8s[reduxIDs[key].id]?.loaded; + const loadError = resourceK8s[reduxIDs[key].id]?.loadError; acc[key] = { data, loaded, loadError }; } else { acc[key] = { diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useModelsLoaded.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useModelsLoaded.ts index fc4af56650f..50644ee9759 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useModelsLoaded.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useModelsLoaded.ts @@ -1,6 +1,5 @@ import { useRef } from 'react'; import { useSelector } from 'react-redux'; -import type { K8sModel } from '../../../api/common-types'; import type { OpenShiftReduxRootState } from './k8s-watch-types'; /** @@ -11,11 +10,9 @@ import type { OpenShiftReduxRootState } from './k8s-watch-types'; */ export const useModelsLoaded = (): boolean => { const ref = useRef(false); - const loaded = useSelector(({ k8s }) => - k8s.getIn(['RESOURCES', 'loaded']), - ); - const inFlight = useSelector(({ k8s }) => - k8s.getIn(['RESOURCES', 'inFlight']), + const loaded = useSelector(({ k8s }) => k8s.RESOURCES?.loaded); + const inFlight = useSelector( + ({ k8s }) => k8s.RESOURCES?.inFlight, ); if (!ref.current && loaded && !inFlight) { diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/k8s-utils.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/k8s-utils.ts index 24208af83f4..d1837a8dcf3 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/k8s-utils.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/k8s-utils.ts @@ -1,4 +1,3 @@ -import { Map as ImmutableMap } from 'immutable'; import * as _ from 'lodash'; import type { K8sModel, MatchExpression, MatchLabels, Selector } from '../../api/common-types'; import type { Options } from '../../api/internal-types'; @@ -173,10 +172,13 @@ export const k8sWatch = ( const modelKey = (model: K8sModel): string => // TODO: Use `referenceForModel` even for known API objects model.crd ? getReferenceForModel(model) : model.kind; -const modelsToMap = (models: K8sModel[]): ImmutableMap => - ImmutableMap().withMutations((map) => { - models.forEach((model) => map.set(modelKey(model), model)); +const modelsToMap = (models: K8sModel[]): Record => { + const map: Record = {}; + models.forEach((model) => { + map[modelKey(model)] = model; }); + return map; +}; /** * Contains static resource definitions for Kubernetes objects. @@ -204,7 +206,7 @@ export const allModels = getK8sModels; export const getNamespacedResources = () => { if (!namespacedResources) { namespacedResources = new Set(); - allModels().forEach((v, k) => { + Object.entries(allModels()).forEach(([k, v]: [string, K8sModel]) => { if (!v.namespaced) { return; } diff --git a/frontend/packages/console-shared/src/components/dashboard/utilization-card/prometheus-hook.ts b/frontend/packages/console-shared/src/components/dashboard/utilization-card/prometheus-hook.ts index 792fa9ae5a0..433d77348d1 100644 --- a/frontend/packages/console-shared/src/components/dashboard/utilization-card/prometheus-hook.ts +++ b/frontend/packages/console-shared/src/components/dashboard/utilization-card/prometheus-hook.ts @@ -1,5 +1,4 @@ import { useEffect, useMemo } from 'react'; -import type { Map as ImmutableMap } from 'immutable'; import { watchPrometheusQuery, stopWatchPrometheusQuery, @@ -20,15 +19,15 @@ export const usePrometheusQuery: UsePrometheusQuery = (query, humanize) => { }; }, [dispatch, query]); - const queryResult = useConsoleSelector(({ dashboards }) => - dashboards.getIn([RESULTS_TYPE.PROMETHEUS, query]), - ) as ImmutableMap; + const queryResult = useConsoleSelector( + ({ dashboards }) => dashboards[RESULTS_TYPE.PROMETHEUS]?.[query], + ); const results = useMemo<[HumanizeResult, any, number]>(() => { - if (!queryResult || !queryResult.get('data')) { + if (!queryResult || !queryResult.data) { return [{}, null, null] as [HumanizeResult, any, number]; } - const value = getInstantVectorStats(queryResult.get('data'))[0]?.y; - return [humanize(value), queryResult.get('loadError'), value]; + const value = getInstantVectorStats(queryResult.data)[0]?.y; + return [humanize(value), queryResult.loadError, value]; }, [queryResult, humanize]); return results; diff --git a/frontend/packages/console-shared/src/components/dynamic-form/utils.ts b/frontend/packages/console-shared/src/components/dynamic-form/utils.ts index 98ce46fdd2e..6789e1a6345 100644 --- a/frontend/packages/console-shared/src/components/dynamic-form/utils.ts +++ b/frontend/packages/console-shared/src/components/dynamic-form/utils.ts @@ -1,6 +1,5 @@ import type { UiSchema } from '@rjsf/core'; import { getSchemaType, getUiOptions } from '@rjsf/core/dist/cjs/utils'; -import * as Immutable from 'immutable'; import type { JSONSchema7 } from 'json-schema'; import * as _ from 'lodash'; import { THOUSAND, MILLION, BILLION } from './const'; @@ -205,11 +204,11 @@ export const getJSONSchemaOrder = ( return {}; } - const uiOrder = Immutable.Set(propertyNames) - .sortBy((property) => - getJSONSchemaPropertySortWeight(property, jsonSchema, uiSchema, currentPath ?? []), - ) - .toJS(); + const uiOrder = [...new Set(propertyNames)].sort( + (a, b) => + getJSONSchemaPropertySortWeight(a, jsonSchema, uiSchema, currentPath ?? []) - + getJSONSchemaPropertySortWeight(b, jsonSchema, uiSchema, currentPath ?? []), + ); return { ...(uiOrder.length > 1 && { 'ui:order': uiOrder }), diff --git a/frontend/packages/console-shared/src/components/formik-fields/CodeEditorField.tsx b/frontend/packages/console-shared/src/components/formik-fields/CodeEditorField.tsx index d4d2d8f6ba5..04f30e10696 100644 --- a/frontend/packages/console-shared/src/components/formik-fields/CodeEditorField.tsx +++ b/frontend/packages/console-shared/src/components/formik-fields/CodeEditorField.tsx @@ -64,7 +64,7 @@ export const CodeEditorField: FC = ({ } const yamlByExtension: string = getYAMLTemplates( templateExtensions?.filter((e) => e.properties.model.kind === kind), - ).getIn([kind, id]); + )?.[kind]?.[id]; return yamlByExtension?.trim() || ''; }, [templateExtensions], diff --git a/frontend/packages/console-shared/src/components/query-browser/QueryBrowser.tsx b/frontend/packages/console-shared/src/components/query-browser/QueryBrowser.tsx index 46c5dea1e00..e89a95a72e9 100644 --- a/frontend/packages/console-shared/src/components/query-browser/QueryBrowser.tsx +++ b/frontend/packages/console-shared/src/components/query-browser/QueryBrowser.tsx @@ -687,12 +687,12 @@ const QueryBrowserWrapped: FC = ({ units, }) => { const { t } = useTranslation('console-shared'); - const hideGraphs = useConsoleSelector(({ observe }) => !!observe.get('hideGraphs')); + const hideGraphs = useConsoleSelector(({ observe }) => !!observe.hideGraphs); const tickInterval = useConsoleSelector( - ({ observe }) => pollInterval ?? observe.getIn(['queryBrowser', 'pollInterval']), + ({ observe }) => pollInterval ?? observe.queryBrowser?.pollInterval, ); - const lastRequestTime = useConsoleSelector(({ observe }) => - observe.getIn(['queryBrowser', 'lastRequestTime']), + const lastRequestTime = useConsoleSelector( + ({ observe }) => observe.queryBrowser?.lastRequestTime, ); const dispatch = useConsoleDispatch(); diff --git a/frontend/packages/console-shared/src/hooks/redux-selectors.ts b/frontend/packages/console-shared/src/hooks/redux-selectors.ts index 0ddf7c978eb..f25fef51d9e 100644 --- a/frontend/packages/console-shared/src/hooks/redux-selectors.ts +++ b/frontend/packages/console-shared/src/hooks/redux-selectors.ts @@ -1,4 +1,4 @@ import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector'; export const useActiveNamespace = (): string => - useConsoleSelector(({ UI }) => UI.get('activeNamespace')); + useConsoleSelector(({ UI }) => UI.activeNamespace); diff --git a/frontend/packages/console-shared/src/hooks/useDashboardResources.ts b/frontend/packages/console-shared/src/hooks/useDashboardResources.ts index 18a33f637da..babf6395a96 100644 --- a/frontend/packages/console-shared/src/hooks/useDashboardResources.ts +++ b/frontend/packages/console-shared/src/hooks/useDashboardResources.ts @@ -35,9 +35,9 @@ export const useDashboardResources: UseDashboardResources = ({ }; }, [dispatch, prometheusQueries, urls]); - const urlResults = useConsoleSelector((state) => state.dashboards.get(RESULTS_TYPE.URL)); - const prometheusResults = useConsoleSelector((state) => - state.dashboards.get(RESULTS_TYPE.PROMETHEUS), + const urlResults = useConsoleSelector((state) => state.dashboards[RESULTS_TYPE.URL]); + const prometheusResults = useConsoleSelector( + (state) => state.dashboards[RESULTS_TYPE.PROMETHEUS], ); return { diff --git a/frontend/packages/console-shared/src/hooks/useLocation.ts b/frontend/packages/console-shared/src/hooks/useLocation.ts index ba5d37fd931..4fe957aedbd 100644 --- a/frontend/packages/console-shared/src/hooks/useLocation.ts +++ b/frontend/packages/console-shared/src/hooks/useLocation.ts @@ -1,3 +1,3 @@ import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector'; -export const useLocation = () => useConsoleSelector(({ UI }) => UI.get('location') ?? ''); +export const useLocation = () => useConsoleSelector(({ UI }) => UI.location ?? ''); diff --git a/frontend/packages/console-shared/src/hooks/useNotificationAlerts.ts b/frontend/packages/console-shared/src/hooks/useNotificationAlerts.ts index e31c784bc6b..64dde4274f8 100644 --- a/frontend/packages/console-shared/src/hooks/useNotificationAlerts.ts +++ b/frontend/packages/console-shared/src/hooks/useNotificationAlerts.ts @@ -26,8 +26,8 @@ export const useNotificationAlerts = ( true, true, ); - const notificationAlerts = useConsoleSelector(({ observe }) => - observe.get('notificationAlerts'), + const notificationAlerts = useConsoleSelector( + ({ observe }) => observe.notificationAlerts, ); const { data: alerts, loaded, loadError } = notificationAlerts ?? emptyNotificationAlerts; diff --git a/frontend/packages/console-shared/src/hooks/useResourceSidebarSamples.ts b/frontend/packages/console-shared/src/hooks/useResourceSidebarSamples.ts index 666b1dc983b..930af05f863 100644 --- a/frontend/packages/console-shared/src/hooks/useResourceSidebarSamples.ts +++ b/frontend/packages/console-shared/src/hooks/useResourceSidebarSamples.ts @@ -1,4 +1,3 @@ -import { Map as ImmutableMap } from 'immutable'; import * as YAML from 'js-yaml'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; @@ -83,256 +82,235 @@ const useClusterRoleBindingSamples = (): Sample[] => { ]; }; -const useDefaultSamples = () => { +const useDefaultSamples = (): Record => { const { t } = useTranslation('console-shared'); const addActions = useExtensions(isAddAction); const catalogItemTypes = useExtensions(isCatalogItemType); const perspectives = useExtensions(isPerspective); const clusterRoleBindingSamples = useClusterRoleBindingSamples(); - return ImmutableMap() - .setIn( - [referenceForModel(BuildConfigModel)], - [ - { - title: t('Build from Dockerfile'), - description: t( - 'A Dockerfile build performs an image build using a Dockerfile in the source repository or specified in build configuration.', - ), - id: 'docker-build', - targetResource: getTargetResource(BuildConfigModel), + return { + [referenceForModel(BuildConfigModel)]: [ + { + title: t('Build from Dockerfile'), + description: t( + 'A Dockerfile build performs an image build using a Dockerfile in the source repository or specified in build configuration.', + ), + id: 'docker-build', + targetResource: getTargetResource(BuildConfigModel), + }, + { + title: t('Source-to-Image (S2I) build'), + description: t( + 'S2I is a tool for building reproducible container images. It produces ready-to-run images by injecting the application source into a container image and assembling a new image.', + ), + id: 's2i-build', + targetResource: getTargetResource(BuildConfigModel), + }, + ], + [referenceForModel(ResourceQuotaModel)]: [ + { + title: t('Set compute resource quota'), + description: t('Limit the total amount of memory and CPU that can be used in a namespace.'), + id: 'rq-compute', + targetResource: getTargetResource(ResourceQuotaModel), + }, + { + title: t('Set maximum count for any resource'), + description: t( + 'Restrict maximum count of each resource so users cannot create more than the allotted amount.', + ), + id: 'rq-counts', + targetResource: getTargetResource(ResourceQuotaModel), + }, + { + title: t('Specify resource quotas for a given storage class'), + description: t( + 'Limit the size and number of persistent volume claims that can be created with a storage class.', + ), + id: 'rq-storageclass', + targetResource: getTargetResource(ResourceQuotaModel), + }, + ], + [referenceForModel(RoleModel)]: [ + { + title: t('Allow reading the resource in API group'), + description: t('This "Role" is allowed to read the resource "Pods" in the core API group.'), + id: 'read-pods-within-ns', + targetResource: getTargetResource(RoleModel), + }, + { + title: t('Allow reading/writing the resource in API group'), + description: t( + 'This "Role" is allowed to read and write the "Deployments" in both the "extensions" and "apps" API groups.', + ), + id: 'read-write-deployment-in-ext-and-apps-apis', + targetResource: getTargetResource(RoleModel), + }, + { + title: t('Allow different access rights to different types of resource and API groups'), + description: t( + 'This "Role" is allowed to read "Pods" and read/write "Jobs" resources in API groups.', + ), + id: 'read-pods-and-read-write-jobs', + targetResource: getTargetResource(RoleModel), + }, + { + title: t('Allow reading a ConfigMap in a specific namespace (for RoleBinding)'), + description: t( + 'This "Role" is allowed to read a "ConfigMap" named "my-config" (must be bound with a "RoleBinding" to limit to a single "ConfigMap" in a single namespace).', + ), + id: 'read-configmap-within-ns', + targetResource: getTargetResource(RoleModel), + }, + ...clusterRoleBindingSamples, + ], + [referenceForModel(ClusterRoleModel)]: clusterRoleBindingSamples, + [referenceForModel(ConsoleLinkModel)]: [ + { + title: t('Add a link to the user menu'), + description: t( + 'The user menu appears in the right side of the masthead below the username.', + ), + id: 'cl-user-menu', + targetResource: getTargetResource(ConsoleLinkModel), + }, + { + title: t('Add a link to the application menu'), + description: t( + 'The application menu appears in the masthead below the 9x9 grid icon. Application menu links can include an optional image and section heading.', + ), + id: 'cl-application-menu', + targetResource: getTargetResource(ConsoleLinkModel), + }, + { + title: t('Add a link to the namespace dashboard'), + description: t( + 'Namespace dashboard links appear on the project dashboard and namespace details pages in a section called "Launcher". Namespace dashboard links can optionally be restricted to a specific namespace or namespaces.', + ), + id: 'cl-namespace-dashboard', + targetResource: getTargetResource(ConsoleLinkModel), + }, + { + title: t('Add a link to the contact mail'), + description: t( + 'The contact mail link appears in the user menu below the username. The link will open the default email client with the email address filled in.', + ), + id: 'cl-contact-mail', + targetResource: getTargetResource(ConsoleLinkModel), + }, + ], + [referenceForModel(ConsoleOperatorConfigModel)]: [ + { + title: t('Add catalog categories'), + description: t( + 'Provides a list of default categories which are shown in the Software Catalog. The categories must be added below customization developerCatalog.', + ), + id: 'devcatalog-categories', + snippet: true, + lazyYaml: () => YAML.dump(defaultCatalogCategories), + targetResource: getTargetResource(ConsoleOperatorConfigModel), + }, + { + title: t('Add project access roles'), + description: t( + 'Provides a list of default roles which are shown in the Project Access. The roles must be added below customization projectAccess.', + ), + id: 'projectaccess-roles', + snippet: true, + lazyYaml: () => YAML.dump(defaultProjectAccessRoles), + targetResource: getTargetResource(ConsoleOperatorConfigModel), + }, + { + title: t('Add page actions'), + description: t( + 'Provides a list of all available actions on the Add page in the Developer perspective. The IDs must be added below customization addPage disabledActions to hide these actions.', + ), + id: 'addpage-actions', + snippet: true, + lazyYaml: () => { + const sortedExtensions = addActions + .slice() + .sort((a, b) => a.properties.id.localeCompare(b.properties.id)); + const yaml = sortedExtensions + .map((extension) => { + const { id, label, description } = extension.properties; + const labelComment = label.split('\n').join('\n # '); + const descriptionComment = description.split('\n').join('\n # '); + return `- # ${labelComment}\n # ${descriptionComment}\n ${id}`; + }) + .join('\n'); + return yaml; }, - { - title: t('Source-to-Image (S2I) build'), - description: t( - 'S2I is a tool for building reproducible container images. It produces ready-to-run images by injecting the application source into a container image and assembling a new image.', - ), - id: 's2i-build', - targetResource: getTargetResource(BuildConfigModel), + targetResource: getTargetResource(ConsoleOperatorConfigModel), + }, + { + title: t('Add sub-catalog types'), + description: t( + 'Provides a list of all the available sub-catalog types which are shown in the Software Catalog. The types must be added below spec customization developerCatalog', + ), + id: 'devcatalog-types', + snippet: true, + lazyYaml: () => { + const enabledTypes = { + state: 'Enabled', + enabled: catalogItemTypes.map((extension) => extension.properties.type), + }; + return YAML.dump(enabledTypes); }, - ], - ) - .setIn( - [referenceForModel(ResourceQuotaModel)], - [ - { - title: t('Set compute resource quota'), - description: t( - 'Limit the total amount of memory and CPU that can be used in a namespace.', - ), - id: 'rq-compute', - targetResource: getTargetResource(ResourceQuotaModel), - }, - { - title: t('Set maximum count for any resource'), - description: t( - 'Restrict maximum count of each resource so users cannot create more than the allotted amount.', - ), - id: 'rq-counts', - targetResource: getTargetResource(ResourceQuotaModel), - }, - { - title: t('Specify resource quotas for a given storage class'), - description: t( - 'Limit the size and number of persistent volume claims that can be created with a storage class.', - ), - id: 'rq-storageclass', - targetResource: getTargetResource(ResourceQuotaModel), - }, - ], - ) - .setIn( - [referenceForModel(RoleModel)], - [ - { - title: t('Allow reading the resource in API group'), - description: t( - 'This "Role" is allowed to read the resource "Pods" in the core API group.', - ), - id: 'read-pods-within-ns', - targetResource: getTargetResource(RoleModel), - }, - { - title: t('Allow reading/writing the resource in API group'), - description: t( - 'This "Role" is allowed to read and write the "Deployments" in both the "extensions" and "apps" API groups.', - ), - id: 'read-write-deployment-in-ext-and-apps-apis', - targetResource: getTargetResource(RoleModel), - }, - { - title: t('Allow different access rights to different types of resource and API groups'), - description: t( - 'This "Role" is allowed to read "Pods" and read/write "Jobs" resources in API groups.', - ), - id: 'read-pods-and-read-write-jobs', - targetResource: getTargetResource(RoleModel), - }, - { - title: t('Allow reading a ConfigMap in a specific namespace (for RoleBinding)'), - description: t( - 'This "Role" is allowed to read a "ConfigMap" named "my-config" (must be bound with a "RoleBinding" to limit to a single "ConfigMap" in a single namespace).', - ), - id: 'read-configmap-within-ns', - targetResource: getTargetResource(RoleModel), - }, - ...clusterRoleBindingSamples, - ], - ) - .setIn([referenceForModel(ClusterRoleModel)], clusterRoleBindingSamples) - .setIn( - [referenceForModel(ConsoleLinkModel)], - [ - { - title: t('Add a link to the user menu'), - description: t( - 'The user menu appears in the right side of the masthead below the username.', - ), - id: 'cl-user-menu', - targetResource: getTargetResource(ConsoleLinkModel), - }, - { - title: t('Add a link to the application menu'), - description: t( - 'The application menu appears in the masthead below the 9x9 grid icon. Application menu links can include an optional image and section heading.', - ), - id: 'cl-application-menu', - targetResource: getTargetResource(ConsoleLinkModel), - }, - { - title: t('Add a link to the namespace dashboard'), - description: t( - 'Namespace dashboard links appear on the project dashboard and namespace details pages in a section called "Launcher". Namespace dashboard links can optionally be restricted to a specific namespace or namespaces.', - ), - id: 'cl-namespace-dashboard', - targetResource: getTargetResource(ConsoleLinkModel), - }, - { - title: t('Add a link to the contact mail'), - description: t( - 'The contact mail link appears in the user menu below the username. The link will open the default email client with the email address filled in.', - ), - id: 'cl-contact-mail', - targetResource: getTargetResource(ConsoleLinkModel), - }, - ], - ) - .setIn( - [referenceForModel(ConsoleOperatorConfigModel)], - [ - { - title: t('Add catalog categories'), - description: t( - 'Provides a list of default categories which are shown in the Software Catalog. The categories must be added below customization developerCatalog.', - ), - id: 'devcatalog-categories', - snippet: true, - lazyYaml: () => YAML.dump(defaultCatalogCategories), - targetResource: getTargetResource(ConsoleOperatorConfigModel), - }, - { - title: t('Add project access roles'), - description: t( - 'Provides a list of default roles which are shown in the Project Access. The roles must be added below customization projectAccess.', - ), - id: 'projectaccess-roles', - snippet: true, - lazyYaml: () => YAML.dump(defaultProjectAccessRoles), - targetResource: getTargetResource(ConsoleOperatorConfigModel), - }, - { - title: t('Add page actions'), - description: t( - 'Provides a list of all available actions on the Add page in the Developer perspective. The IDs must be added below customization addPage disabledActions to hide these actions.', - ), - id: 'addpage-actions', - snippet: true, - lazyYaml: () => { - const sortedExtensions = addActions - .slice() - .sort((a, b) => a.properties.id.localeCompare(b.properties.id)); - const yaml = sortedExtensions - .map((extension) => { - const { id, label, description } = extension.properties; - const labelComment = label.split('\n').join('\n # '); - const descriptionComment = description.split('\n').join('\n # '); - return `- # ${labelComment}\n # ${descriptionComment}\n ${id}`; - }) - .join('\n'); - return yaml; - }, - targetResource: getTargetResource(ConsoleOperatorConfigModel), - }, - { - title: t('Add sub-catalog types'), - description: t( - 'Provides a list of all the available sub-catalog types which are shown in the Software Catalog. The types must be added below spec customization developerCatalog', - ), - id: 'devcatalog-types', - snippet: true, - lazyYaml: () => { - const enabledTypes = { - state: 'Enabled', - enabled: catalogItemTypes.map((extension) => extension.properties.type), + targetResource: getTargetResource(ConsoleOperatorConfigModel), + }, + { + title: t('Add user perspectives'), + description: t( + 'Provides a list of all the available user perspectives which are shown in the perspective dropdown. The perspectives must be added below spec customization.', + ), + id: 'user-perspectives', + snippet: true, + lazyYaml: () => { + const yaml = perspectives.map((extension) => { + const { id } = extension.properties; + return { + id, + visibility: { + state: 'Enabled', + }, }; - return YAML.dump(enabledTypes); - }, - targetResource: getTargetResource(ConsoleOperatorConfigModel), - }, - { - title: t('Add user perspectives'), - description: t( - 'Provides a list of all the available user perspectives which are shown in the perspective dropdown. The perspectives must be added below spec customization.', - ), - id: 'user-perspectives', - snippet: true, - lazyYaml: () => { - const yaml = perspectives.map((extension) => { - const { id } = extension.properties; - return { - id, - visibility: { - state: 'Enabled', - }, - }; - }); - return YAML.dump(yaml); - }, - targetResource: getTargetResource(ConsoleOperatorConfigModel), + }); + return YAML.dump(yaml); }, - { - title: t('Add pinned resources'), - description: t( - 'Provides a list of resources to be pinned on the Developer perspective navigation. The pinned resources must be added below spec customization perspectives.', - ), - id: 'dev-pinned-resources', - snippet: true, - lazyYaml: () => YAML.dump(samplePinnedResources), - targetResource: getTargetResource(ConsoleOperatorConfigModel), - }, - ], - ) - .setIn( - [referenceForModel(PodDisruptionBudgetModel)], - [ - { - title: t('Set maxUnavailable to 0'), - description: t( - 'An eviction is allowed if at most 0 pods selected by "selector" are unavailable after the eviction.', - ), - id: 'pdb-max-unavailable', - targetResource: getTargetResource(PodDisruptionBudgetModel), - }, - { - title: t('Set minAvailable to 25%'), - description: t( - 'An eviction is allowed if at least 25% of pods selected by "selector" will still be available after the eviction.', - ), - id: 'pdb-min-available', - targetResource: getTargetResource(PodDisruptionBudgetModel), - }, - ], - ); + targetResource: getTargetResource(ConsoleOperatorConfigModel), + }, + { + title: t('Add pinned resources'), + description: t( + 'Provides a list of resources to be pinned on the Developer perspective navigation. The pinned resources must be added below spec customization perspectives.', + ), + id: 'dev-pinned-resources', + snippet: true, + lazyYaml: () => YAML.dump(samplePinnedResources), + targetResource: getTargetResource(ConsoleOperatorConfigModel), + }, + ], + [referenceForModel(PodDisruptionBudgetModel)]: [ + { + title: t('Set maxUnavailable to 0'), + description: t( + 'An eviction is allowed if at most 0 pods selected by "selector" are unavailable after the eviction.', + ), + id: 'pdb-max-unavailable', + targetResource: getTargetResource(PodDisruptionBudgetModel), + }, + { + title: t('Set minAvailable to 25%'), + description: t( + 'An eviction is allowed if at least 25% of pods selected by "selector" will still be available after the eviction.', + ), + id: 'pdb-min-available', + targetResource: getTargetResource(PodDisruptionBudgetModel), + }, + ], + }; }; export const useResourceSidebarSamples = ( @@ -354,7 +332,7 @@ export const useResourceSidebarSamples = ( ) : []; - const existingSamples = defaultSamples.get(referenceForModel(kindObj)) || []; + const existingSamples = defaultSamples[referenceForModel(kindObj)] || []; const extensionSamples = !_.isEmpty(yamlSamplesData) ? yamlSamplesData.map((sample: K8sResourceKind) => ({ id: sample.metadata.uid, diff --git a/frontend/packages/console-shared/src/hooks/useUtilizationDuration.ts b/frontend/packages/console-shared/src/hooks/useUtilizationDuration.ts index fdb68947e34..6feeac88922 100644 --- a/frontend/packages/console-shared/src/hooks/useUtilizationDuration.ts +++ b/frontend/packages/console-shared/src/hooks/useUtilizationDuration.ts @@ -10,14 +10,13 @@ export const useUtilizationDuration: UseUtilizationDuration = ( ) => { const dispatch = useConsoleDispatch(); const duration = - useConsoleSelector(({ UI }) => UI.getIn(['utilizationDuration', 'duration'])) ?? - DEFAULT_DURATION; - const storeEndDate = useConsoleSelector(({ UI }) => - UI.getIn(['utilizationDuration', 'endDate']), + useConsoleSelector(({ UI }) => UI.utilizationDuration?.duration) ?? DEFAULT_DURATION; + const storeEndDate = useConsoleSelector( + ({ UI }) => UI.utilizationDuration?.endTime, ); const endDate = useMemo(() => storeEndDate ?? new Date(), [storeEndDate]); const selectedKey = - useConsoleSelector(({ UI }) => UI.getIn(['utilizationDuration', 'selectedKey'])) ?? + useConsoleSelector(({ UI }) => UI.utilizationDuration?.selectedKey) ?? DEFAULT_DURATION_KEY; const startDate = new Date(endDate.getTime() - duration); const updateEndDate = useCallback( diff --git a/frontend/packages/container-security/integration-tests/bad-pods.ts b/frontend/packages/container-security/integration-tests/bad-pods.ts index b4a5955b283..bd42ebe0392 100644 --- a/frontend/packages/container-security/integration-tests/bad-pods.ts +++ b/frontend/packages/container-security/integration-tests/bad-pods.ts @@ -12,11 +12,11 @@ export const fakeVulnFor = (priority: Priority): ImageManifestVuln => ({ 'default/3scale-operator-7864b9bb5d-frhnt': 'true', }, name: `sha256.e94c22ba519b1e0ae035e1786a7d2eb9425d62ff60be8ba2dc6b86234540bcb${ - vulnPriority.get(priority).index + vulnPriority[priority].index }`, namespace: 'default', resourceVersion: '3082821', - uid: `74b640b4-0503-4fbe-9354-1939630e082${vulnPriority.get(priority).index}`, + uid: `74b640b4-0503-4fbe-9354-1939630e082${vulnPriority[priority].index}`, }, spec: { features: [ @@ -33,7 +33,7 @@ export const fakeVulnFor = (priority: Priority): ImageManifestVuln => ({ link: 'https://access.redhat.com/errata/RHSA-2019:1880', name: 'RHSA-2019:1880', namespaceName: 'centos:7', - severity: vulnPriority.get(priority).title, + severity: vulnPriority[priority].title, }, ], }, @@ -76,7 +76,7 @@ export const fakeVulnFor = (priority: Priority): ImageManifestVuln => ({ }, fixableCount: 0, highCount: 0, - highestSeverity: vulnPriority.get(priority).title, + highestSeverity: vulnPriority[priority].title, lastUpdate: '2019-12-27 22:00:48.328470155 +0000 UTC', lowCount: 1, mediumCount: 1, diff --git a/frontend/packages/container-security/src/components/ImageVulnerabilityToggleGroup.tsx b/frontend/packages/container-security/src/components/ImageVulnerabilityToggleGroup.tsx index 16178f5ef01..6d45e6a2c49 100644 --- a/frontend/packages/container-security/src/components/ImageVulnerabilityToggleGroup.tsx +++ b/frontend/packages/container-security/src/components/ImageVulnerabilityToggleGroup.tsx @@ -6,6 +6,7 @@ import { RhUiWarningFillIcon } from '@patternfly/react-icons'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { ConsoleEmptyState } from '@console/internal/components/utils'; +import type { Priority } from '../const'; import { totalVulnFor, vulnPriority } from '../const'; import type { ImageManifestVuln, Vulnerability } from '../types'; import { @@ -75,14 +76,12 @@ const ImageVulnerabilityToggleGroup: FC = ({
priority.color.value).toArray()} - data={vulnPriority - .map((priority, key) => ({ - label: priority.title, - x: priority.value, - y: totalVulnFor(key)(totalSelectedVuln), - })) - .toArray()} + colorScale={Object.values(vulnPriority).map((priority) => priority.color.value)} + data={Object.entries(vulnPriority).map(([key, priority]) => ({ + label: priority.title, + x: priority.value, + y: totalVulnFor(key as Priority)(totalSelectedVuln), + }))} title={t('{{totalSelectedVulnCount, number}} total', { totalSelectedVulnCount, })} @@ -112,20 +111,18 @@ const ImageVulnerabilityToggleGroup: FC = ({ })}
- {vulnPriority - .map((v, k) => - totalVulnFor(k)(totalSelectedVuln) > 0 ? ( - - {' '} - {totalVulnFor(k)(totalSelectedVuln)}{' '} - {t('{{title}} vulnerabilities.', { title: v.title })} - - ) : null, - ) - .toArray()} + {Object.entries(vulnPriority).map(([k, v]) => + totalVulnFor(k as Priority)(totalSelectedVuln) > 0 ? ( + + {' '} + {totalVulnFor(k as Priority)(totalSelectedVuln)}{' '} + {t('{{title}} vulnerabilities.', { title: v.title })} + + ) : null, + )}
diff --git a/frontend/packages/container-security/src/components/image-manifest-vuln.tsx b/frontend/packages/container-security/src/components/image-manifest-vuln.tsx index 6465eb85f62..2026fee9b36 100644 --- a/frontend/packages/container-security/src/components/image-manifest-vuln.tsx +++ b/frontend/packages/container-security/src/components/image-manifest-vuln.tsx @@ -41,7 +41,7 @@ import { EmptyStateResourceBadge } from '@console/shared/src/components/badges/E import PaneBody from '@console/shared/src/components/layout/PaneBody'; import { ExternalLink } from '@console/shared/src/components/links/ExternalLink'; import { GreenCheckCircleIcon } from '@console/shared/src/components/status/icons'; -import { vulnPriority, totalFor, priorityFor } from '../const'; +import { totalFor, priorityFor } from '../const'; import { ImageManifestVulnModel } from '../models'; import type { ImageManifestVuln } from '../types'; import ImageVulnerabilitiesList from './ImageVulnerabilitiesList'; @@ -382,9 +382,7 @@ const ContainerVulnerabilities: FC = (props) => { name={vuln.metadata.name} namespace={props.pod.metadata.namespace} displayName={`${totalFor( - vulnPriority.findKey( - ({ title }) => _.get(vuln.status, 'highestSeverity') === title, - ), + priorityFor(_.get(vuln.status, 'highestSeverity')).value, )(vuln)} ${vuln.status.highestSeverity}`} hideIcon /> diff --git a/frontend/packages/container-security/src/components/summary.tsx b/frontend/packages/container-security/src/components/summary.tsx index d989fe60e39..10aaebdbc6f 100644 --- a/frontend/packages/container-security/src/components/summary.tsx +++ b/frontend/packages/container-security/src/components/summary.tsx @@ -97,34 +97,27 @@ export const SecurityBreakdownPopup: FC = ({
- {vulnPriority - .map((priority) => - !_.isEmpty(vulnsFor(priority.value)) ? ( -
-
- -   - {vulnsFor(priority.value).length} {priority.title} -
+ {Object.values(vulnPriority).map((priority) => + !_.isEmpty(vulnsFor(priority.value)) ? ( +
+
+ +   + {vulnsFor(priority.value).length} {priority.title}
- ) : null, - ) - .toArray()} +
+ ) : null, + )}
priority.color.value).toArray()} - data={vulnPriority - .map((priority) => ({ - label: priority.title, - x: priority.value, - y: vulnsFor(priority.value).length, - })) - .toArray()} + colorScale={Object.values(vulnPriority).map((priority) => priority.color.value)} + data={Object.values(vulnPriority).map((priority) => ({ + label: priority.title, + x: priority.value, + y: vulnsFor(priority.value).length, + }))} title={t('{{vulnImageCount, number}} total', { vulnImageCount: resource.length, })} diff --git a/frontend/packages/container-security/src/const.ts b/frontend/packages/container-security/src/const.ts index 465a4e90862..7b2d2c6eba6 100644 --- a/frontend/packages/container-security/src/const.ts +++ b/frontend/packages/container-security/src/const.ts @@ -6,7 +6,6 @@ import { chart_color_red_orange_300 as redorange300 } from '@patternfly/react-to import { chart_color_red_orange_400 as redorange400 } from '@patternfly/react-tokens/dist/esm/chart_color_red_orange_400'; import { chart_color_yellow_400 as yellow400 } from '@patternfly/react-tokens/dist/esm/chart_color_yellow_400'; /* eslint-enable @typescript-eslint/naming-convention */ -import { Map as ImmutableMap } from 'immutable'; import type { ImageManifestVuln } from './types'; export enum Priority { @@ -19,8 +18,18 @@ export enum Priority { Unknown = 'Unknown', } -export const vulnPriority = ImmutableMap() - .set(Priority.Defcon1, { +export type VulnPriorityDescription = { + color: any; + description: string; + index: number; + level: 'error' | 'warning' | 'info'; + score: number; + title: string; + value: Priority; +}; + +export const vulnPriority: Record = Object.freeze({ + [Priority.Defcon1]: { color: redorange400, description: 'Defcon1 is a Critical problem which has been manually highlighted by the Quay team. It requires immediate attention.', @@ -28,9 +37,9 @@ export const vulnPriority = ImmutableMap() level: 'error', score: 11, title: 'Defcon 1', - value: 'Defcon1', - }) - .set(Priority.Critical, { + value: Priority.Defcon1, + }, + [Priority.Critical]: { color: redorange300, description: 'Critical is a world-burning problem, exploitable for nearly all people in a installation of the package. Includes remote root privilege escalations, or massive data loss.', @@ -38,9 +47,9 @@ export const vulnPriority = ImmutableMap() level: 'error', score: 10, title: 'Critical', - value: 'Critical', - }) - .set(Priority.High, { + value: Priority.Critical, + }, + [Priority.High]: { color: redorange100, description: 'High is a real problem, exploitable for many people in a default installation. Includes serious remote denial of services, local root privilege escalations, or data loss.', @@ -48,9 +57,9 @@ export const vulnPriority = ImmutableMap() level: 'warning', score: 9, title: 'High', - value: 'High', - }) - .set(Priority.Medium, { + value: Priority.High, + }, + [Priority.Medium]: { color: orange300, description: 'Medium is a real security problem, and is exploitable for many people. Includes network daemon denial of service attacks, cross-site scripting, and gaining user privileges.', @@ -58,9 +67,9 @@ export const vulnPriority = ImmutableMap() level: 'warning', score: 6, title: 'Medium', - value: 'Medium', - }) - .set(Priority.Low, { + value: Priority.Medium, + }, + [Priority.Low]: { color: yellow400, description: 'Low is a security problem, but is hard to exploit due to environment, requires a user-assisted attack, a small install base, or does very little damage.', @@ -68,9 +77,9 @@ export const vulnPriority = ImmutableMap() level: 'warning', score: 3, title: 'Low', - value: 'Low', - }) - .set(Priority.Negligible, { + value: Priority.Low, + }, + [Priority.Negligible]: { color: black500, description: 'Negligible is technically a security problem, but is only theoretical in nature, requires a very special situation, has almost no install base, or does no real damage.', @@ -78,9 +87,9 @@ export const vulnPriority = ImmutableMap() level: 'info', score: 1, title: 'Negligible', - value: 'Negligible', - }) - .set(Priority.Unknown, { + value: Priority.Negligible, + }, + [Priority.Unknown]: { color: black500, description: 'Unknown is either a security problem that has not been assigned to a priority yet or a priority that our system did not recognize', @@ -88,18 +97,9 @@ export const vulnPriority = ImmutableMap() level: 'info', score: 0, title: 'Unknown', - value: 'Unknown', - }); - -export type VulnPriorityDescription = { - color: any; - description: string; - index: number; - level: 'error' | 'warning' | 'info'; - score: number; - title: string; - value: string; -}; + value: Priority.Unknown, + }, +}); export const totalFor = (priority: Priority) => (obj: ImageManifestVuln) => { switch (priority) { @@ -148,12 +148,14 @@ export const totalVulnFor = } }; -const vulnPriorityByTitle = vulnPriority.mapEntries( - ([, vulnPriorityDescription]: [Priority, VulnPriorityDescription]) => [ - vulnPriorityDescription.title, - vulnPriorityDescription, - ], -) as ImmutableMap; +const vulnPriorityByTitle: Record = Object.values( + vulnPriority, +).reduce( + (acc, desc) => ({ ...acc, [desc.title]: desc }), + {} as Record, +); export const priorityFor = (severityTitle: string) => - vulnPriorityByTitle.get(severityTitle) || vulnPriority.get(Priority.Unknown); + Object.prototype.hasOwnProperty.call(vulnPriorityByTitle, severityTitle) + ? vulnPriorityByTitle[severityTitle] + : vulnPriority[Priority.Unknown]; diff --git a/frontend/packages/dev-console/src/components/catalog/PinnedResourcesConfiguration.tsx b/frontend/packages/dev-console/src/components/catalog/PinnedResourcesConfiguration.tsx index ad4a492ac49..31a07bb80e7 100644 --- a/frontend/packages/dev-console/src/components/catalog/PinnedResourcesConfiguration.tsx +++ b/frontend/packages/dev-console/src/components/catalog/PinnedResourcesConfiguration.tsx @@ -3,8 +3,7 @@ import { useState, useMemo, memo, useEffect } from 'react'; import { FormHelperText, FormSection, Icon, Tooltip } from '@patternfly/react-core'; import { DualListSelector } from '@patternfly/react-core/deprecated'; import * as fuzzy from 'fuzzysearch'; -import type { Map as ImmutableMap } from 'immutable'; -import { Set as ImmutableSet } from 'immutable'; +import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import type { @@ -38,12 +37,12 @@ import { PerspectiveVisibilityState } from '@console/shared/src/utils/override-p import './PinnedResourcesConfiguration.scss'; // skip duplicate resources. -const skipGroups = ImmutableSet([ +const skipGroups = new Set([ // Prefer rbac.authorization.k8s.io/v1, which has the same resources. 'authorization.openshift.io', ]); -const skipResources = ImmutableSet([ +const skipResources = new Set([ // Prefer core/v1 'events.k8s.io/v1beta1.Event', ]); @@ -63,7 +62,7 @@ type DefaultPins = { type PinnedResourcesConfigurationProps = { readonly: boolean; groupVersionMap: DiscoveryResources['groupVersionMap']; - allK8sModels: ImmutableMap; + allK8sModels: Record; }; const PinnedResourcesConfiguration: FC = ({ @@ -93,33 +92,37 @@ const PinnedResourcesConfiguration: FC = ({ const resources = useMemo( () => allK8sModels - ?.filter(({ apiGroup, apiVersion, kind, verbs }) => { - if (skipGroups.has(apiGroup) || skipResources.has(`${apiGroup}/${apiVersion}.${kind}`)) { - return false; - } + ? Object.values(allK8sModels) + .filter(({ apiGroup, apiVersion, kind, verbs }) => { + if ( + skipGroups.has(apiGroup) || + skipResources.has(`${apiGroup}/${apiVersion}.${kind}`) + ) { + return false; + } - // Only show resources that can be listed. - if (!verbs?.some((v) => v === 'list')) { - return false; - } + // Only show resources that can be listed. + if (!verbs?.some((v) => v === 'list')) { + return false; + } - // Only show preferred version for resources in the same API group. - const preferred = (m: K8sKind) => - groupVersionMap?.[m.apiGroup]?.preferredVersion === m.apiVersion; + // Only show preferred version for resources in the same API group. + const preferred = (m: K8sKind) => + groupVersionMap?.[m.apiGroup]?.preferredVersion === m.apiVersion; - const sameGroupKind = (m: K8sKind) => - m.kind === kind && m.apiGroup === apiGroup && m.apiVersion !== apiVersion; + const sameGroupKind = (m: K8sKind) => + m.kind === kind && m.apiGroup === apiGroup && m.apiVersion !== apiVersion; - return !allK8sModels.find((m) => sameGroupKind(m) && preferred(m)); - }) - .toOrderedMap() - .sortBy(({ kind, apiGroup }) => `${kind} ${apiGroup}`), + return !Object.values(allK8sModels).find((m) => sameGroupKind(m) && preferred(m)); + }) + .sort((a, b) => `${a.kind} ${a.apiGroup}`.localeCompare(`${b.kind} ${b.apiGroup}`)) + : [], [allK8sModels, groupVersionMap], ); // Track duplicate names so we know when to show the group. - const kinds = resources.groupBy((m) => m.kind); - const isDup = (kind) => kinds.get(kind).size > 1; + const kinds = useMemo(() => _.groupBy(resources, (m) => m.kind), [resources]); + const isDup = (kind) => kinds[kind]?.length > 1; type ItemProps = { title?: string; model?: K8sKind }; @@ -198,11 +201,9 @@ const PinnedResourcesConfiguration: FC = ({ const items = useMemo( () => - resources - .map((model: K8sKind) => ( - - )) - .toArray(), + resources.map((model: K8sKind) => ( + + )), [resources, t, Item], ); @@ -327,8 +328,8 @@ const PinnedResourcesConfiguration: FC = ({ }; const mapStateToProps = (state: RootState) => ({ - groupVersionMap: state.k8s.getIn(['RESOURCES', 'groupToVersionMap']), - allK8sModels: state.k8s.getIn(['RESOURCES', 'models']), + groupVersionMap: state.k8s.RESOURCES?.groupToVersionMap, + allK8sModels: state.k8s.RESOURCES?.models, }); export default connect(mapStateToProps)(PinnedResourcesConfiguration); diff --git a/frontend/packages/dev-console/src/components/hpa/hpa-utils.ts b/frontend/packages/dev-console/src/components/hpa/hpa-utils.ts index 48472ad7258..1a106509940 100644 --- a/frontend/packages/dev-console/src/components/hpa/hpa-utils.ts +++ b/frontend/packages/dev-console/src/components/hpa/hpa-utils.ts @@ -46,9 +46,7 @@ export const getRequestsWarning = (resource: K8sResourceKind): string | null => return null; }; -const defaultHPAYAML = baseTemplates - .get(referenceForModel(HorizontalPodAutoscalerModel)) - .get('default'); +const defaultHPAYAML = baseTemplates[referenceForModel(HorizontalPodAutoscalerModel)]?.default; const createScaleTargetRef = (resource: K8sResourceKind) => ({ apiVersion: resource.apiVersion, diff --git a/frontend/packages/knative-plugin/src/topology/knative-topology-utils.ts b/frontend/packages/knative-plugin/src/topology/knative-topology-utils.ts index 84aa56ad0b9..fd6df3211d6 100644 --- a/frontend/packages/knative-plugin/src/topology/knative-topology-utils.ts +++ b/frontend/packages/knative-plugin/src/topology/knative-topology-utils.ts @@ -129,8 +129,7 @@ const getKnNodeModelProps = (type: string) => { * returns if event source is enabled or not * @param Flags */ -export const getEventSourceStatus = ({ FLAGS }: RootState): boolean => - FLAGS.get(FLAG_KNATIVE_EVENTING); +export const getEventSourceStatus = ({ FLAGS }: RootState): boolean => FLAGS[FLAG_KNATIVE_EVENTING]; /** * fetch the parent resource from a resource diff --git a/frontend/packages/operator-lifecycle-manager/src/components/deprecated-operator-warnings/use-deprecated-operator-warnings.ts b/frontend/packages/operator-lifecycle-manager/src/components/deprecated-operator-warnings/use-deprecated-operator-warnings.ts index c9e9f31692c..ff533036476 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/deprecated-operator-warnings/use-deprecated-operator-warnings.ts +++ b/frontend/packages/operator-lifecycle-manager/src/components/deprecated-operator-warnings/use-deprecated-operator-warnings.ts @@ -7,14 +7,14 @@ import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector export const useDeprecatedOperatorWarnings = () => { const dispatch = useConsoleDispatch(); - const deprecatedPackage = useConsoleSelector((state) => - state.UI.getIn(['deprecatedOperator', 'package']), + const deprecatedPackage = useConsoleSelector( + (state) => state.UI.deprecatedOperator?.package, ); - const deprecatedChannel = useConsoleSelector((state) => - state.UI.getIn(['deprecatedOperator', 'channel']), + const deprecatedChannel = useConsoleSelector( + (state) => state.UI.deprecatedOperator?.channel, ); - const deprecatedVersion = useConsoleSelector((state) => - state.UI.getIn(['deprecatedOperator', 'version']), + const deprecatedVersion = useConsoleSelector( + (state) => state.UI.deprecatedOperator?.version, ); const setDeprecatedPackage = useCallback( diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/spec-descriptor-input.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/spec-descriptor-input.tsx index 76bc3cdc37e..2b0769fe82d 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/spec-descriptor-input.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/spec-descriptor-input.tsx @@ -1,4 +1,3 @@ -import * as Immutable from 'immutable'; import { NodeAffinityField, PodAffinityField, @@ -17,7 +16,7 @@ import { } from '@console/shared/src/components/dynamic-form/widgets'; import { SpecCapability } from '../types'; -export const capabilityFieldMap = Immutable.Map({ +export const capabilityFieldMap: Record = Object.freeze({ [SpecCapability.nodeAffinity]: NodeAffinityField, [SpecCapability.podAffinity]: PodAffinityField, [SpecCapability.podAntiAffinity]: PodAffinityField, @@ -25,7 +24,7 @@ export const capabilityFieldMap = Immutable.Map({ [SpecCapability.updateStrategy]: UpdateStrategyField, }); -export const capabilityWidgetMap = Immutable.Map({ +export const capabilityWidgetMap: Record = Object.freeze({ [SpecCapability.hidden]: 'hidden', [SpecCapability.imagePullPolicy]: ImagePullPolicyWidget, [SpecCapability.booleanSwitch]: SwitchWidget, diff --git a/frontend/packages/operator-lifecycle-manager/src/components/install-plan.tsx b/frontend/packages/operator-lifecycle-manager/src/components/install-plan.tsx index fa59616a18e..dab7eee4f89 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/install-plan.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/install-plan.tsx @@ -16,7 +16,6 @@ import { } from '@patternfly/react-core'; import { css } from '@patternfly/react-styles'; import { sortable, Table as PFTable, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; -import { Map as ImmutableMap, Set as ImmutableSet, fromJS } from 'immutable'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { useParams, Link, useNavigate } from 'react-router'; @@ -221,13 +220,18 @@ export const InstallPlansList = requireOperatorGroup((props: InstallPlansListPro const getCatalogSources = ( installPlan: InstallPlanKind, -): { sourceName: string; sourceNamespace: string }[] => - _.reduce( - installPlan?.status?.plan || [], - (accumulator, { resource: { sourceName, sourceNamespace } }) => - accumulator.add(fromJS({ sourceName, sourceNamespace })), - ImmutableSet(), - ).toJS(); +): { sourceName: string; sourceNamespace: string }[] => { + const seen = new Set(); + const result: { sourceName: string; sourceNamespace: string }[] = []; + (installPlan?.status?.plan || []).forEach(({ resource: { sourceName, sourceNamespace } }) => { + const key = `${sourceNamespace}/${sourceName}`; + if (!seen.has(key)) { + seen.add(key); + result.push({ sourceName, sourceNamespace }); + } + }); + return result; +}; export const InstallPlansPage: FC = (props) => { const { t } = useTranslation('olm'); @@ -416,12 +420,7 @@ export const InstallPlanPreview: FC = ({ obj, hideAppro ); const plan = obj?.status?.plan || []; - const stepsByCSV = plan - .reduce( - (acc, step) => acc.update(step.resolving, [], (steps) => steps.concat([step])), - ImmutableMap(), - ) - .toArray(); + const stepsByCSV = Object.values(_.groupBy(plan, 'resolving')) as Step[][]; const approve = () => k8sPatch(InstallPlanModel, obj, [{ op: 'replace', path: '/spec/approved', value: true }]) diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx index 1e2226951db..cbbdeeddb01 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx @@ -21,7 +21,6 @@ import { } from '@patternfly/react-core'; import { RhUiMinusCircleIcon, RhUiAddCircleFillIcon } from '@patternfly/react-icons'; import { css } from '@patternfly/react-styles'; -import * as Immutable from 'immutable'; import type { JSONSchema6, JSONSchema6TypeName } from 'json-schema'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; @@ -192,7 +191,7 @@ const defaultValueFor = (capabilities: Capability[]): any => { // Resource requirement fields if (capabilities.includes(SpecCapability.resourceRequirements)) { - return Immutable.fromJS({ + return { limits: { cpu: '', memory: '', @@ -203,23 +202,24 @@ const defaultValueFor = (capabilities: Capability[]): any => { memory: '', 'ephemeral-storage': '', }, - }); + }; } // Update strategy if (capabilities.includes(SpecCapability.updateStrategy)) { - return Immutable.fromJS({ + return { type: 'RollingUpdate', rollingUpdate: { maxUnavailable: '', maxSurge: '', }, - }); + }; } // Node and pod affinities if (capabilities.includes(SpecCapability.nodeAffinity)) { - return Immutable.fromJS(DEFAULT_NODE_AFFINITY).setIn( + return _.set( + _.cloneDeep(DEFAULT_NODE_AFFINITY), ['preferredDuringSchedulingIgnoredDuringExecution', 'weight'], '', ); @@ -229,7 +229,8 @@ const defaultValueFor = (capabilities: Capability[]): any => { capabilities.includes(SpecCapability.podAffinity) || capabilities.includes(SpecCapability.podAntiAffinity) ) { - return Immutable.fromJS(DEFAULT_POD_AFFINITY).setIn( + return _.set( + _.cloneDeep(DEFAULT_POD_AFFINITY), ['preferredDuringSchedulingIgnoredDuringExecution', 'weight'], '', ); @@ -512,23 +513,21 @@ export const DEPRECATED_CreateOperandForm: FC = ({ const { t } = useTranslation('olm'); const params = useParams(); const navigate = useNavigate(); - const immutableFormData = Immutable.fromJS(formData); const handleFormDataUpdate = (path: string, value: any): void => { const { regexMatch, index, pathBeforeIndex, pathAfterIndex } = parseArrayPath(path); - // Immutable will not initialize a deep path as a List if it includes an integer, so we need to manually - // initialize non-existent array properties to a List instance before updating state at that path. if (regexMatch && index === 0) { - const existing = immutableFormData.getIn([...pathToArray(pathBeforeIndex), 0]); - const item = Immutable.Map(existing || {}).setIn(pathToArray(pathAfterIndex), value); - const list = Immutable.List([item]); - onChange(immutableFormData.setIn(pathToArray(pathBeforeIndex), list).toJS()); + const existing = _.get(formData, [...pathToArray(pathBeforeIndex), 0]) || {}; + const item = _.set(_.cloneDeep(existing), pathToArray(pathAfterIndex), value); + onChange(_.set(_.cloneDeep(formData), pathToArray(pathBeforeIndex), [item])); } - onChange(immutableFormData.setIn(pathToArray(path), value).toJS()); + onChange(_.set(_.cloneDeep(formData), pathToArray(path), value)); }; const handleFormDataDelete = (path) => { - onChange(immutableFormData.deleteIn(pathToArray(path)).toJS()); + const cloned = _.cloneDeep(formData); + _.unset(cloned, pathToArray(path)); + onChange(cloned); }; // Map providedAPI spec descriptors and openAPI spec properties to OperandField[] array @@ -563,9 +562,9 @@ export const DEPRECATED_CreateOperandForm: FC = ({ }); const labelTags = useMemo(() => { - const formValue = immutableFormData.getIn(['metadata', 'labels']); - return SelectorInput.arrayify(_.isFunction(formValue?.toJS) ? formValue.toJS() : {}); - }, [immutableFormData]); + const formValue = _.get(formData, ['metadata', 'labels']); + return SelectorInput.arrayify(_.isObject(formValue) && !_.isArray(formValue) ? formValue : {}); + }, [formData]); const [error, setError] = useState(); @@ -668,15 +667,15 @@ export const DEPRECATED_CreateOperandForm: FC = ({ })); }, [groupFields]); - const getFormData = (path): any => immutableFormData.getIn(pathToArray(path)); + const getFormData = (path): any => _.get(formData, pathToArray(path)); const submit = (e) => { e.preventDefault(); k8sCreate( model, model.namespaced - ? immutableFormData.setIn(['metadata', 'namespace'], params.ns).toJS() - : immutableFormData.toJS(), + ? _.set(_.cloneDeep(formData), ['metadata', 'namespace'], params.ns) + : _.cloneDeep(formData), ) .then((res) => postFormCallback(res)) .then(() => next && navigate(next)) @@ -741,9 +740,9 @@ export const DEPRECATED_CreateOperandForm: FC = ({ {t('Limits')} handleFormDataUpdate(`${path}.${cpuLimitsPath}`, value)} onChangeMemory={(value) => handleFormDataUpdate(`${path}.${memoryLimitsPath}`, value) @@ -759,9 +758,9 @@ export const DEPRECATED_CreateOperandForm: FC = ({ {t('Requests')} handleFormDataUpdate(`${path}.${cpuRequestsPath}`, value)} onChangeMemory={(value) => handleFormDataUpdate(`${path}.${memoryRequestsPath}`, value) @@ -857,9 +856,9 @@ export const DEPRECATED_CreateOperandForm: FC = ({ const maxSurgePath = `rollingUpdate.maxSurge`; return ( handleFormDataUpdate(`${path}.type`, value)} onChangeMaxUnavailable={(value) => handleFormDataUpdate(`${path}.${maxUnavailablePath}`, value) @@ -902,8 +901,8 @@ export const DEPRECATED_CreateOperandForm: FC = ({ return (
handleFormDataUpdate(path, Immutable.fromJS(value))} + affinity={currentValue as NodeAffinityType} + onChange={(value) => handleFormDataUpdate(path, value)} uid={id} />
@@ -916,8 +915,8 @@ export const DEPRECATED_CreateOperandForm: FC = ({ return (
handleFormDataUpdate(path, Immutable.fromJS(value))} + affinity={currentValue} + onChange={(value) => handleFormDataUpdate(path, value)} uid={id} />
@@ -1127,7 +1126,7 @@ export const DEPRECATED_CreateOperandForm: FC = ({ onChange={({ target: { value } }) => handleFormDataUpdate('metadata.name', value) } - value={immutableFormData.getIn(['metadata', 'name']) || 'example'} + value={_.get(formData, ['metadata', 'name']) || 'example'} id="DEPRECATED_root_metadata_name" required /> @@ -1143,10 +1142,7 @@ export const DEPRECATED_CreateOperandForm: FC = ({ - handleFormDataUpdate( - 'metadata.labels', - Immutable.fromJS(SelectorInput.objectify(value)), - ) + handleFormDataUpdate('metadata.labels', SelectorInput.objectify(value)) } tags={labelTags} /> diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/useShowOperandsInAllNamespaces.ts b/frontend/packages/operator-lifecycle-manager/src/components/operand/useShowOperandsInAllNamespaces.ts index 82819df9755..36ce2a6987b 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/useShowOperandsInAllNamespaces.ts +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/useShowOperandsInAllNamespaces.ts @@ -8,8 +8,8 @@ type UseShowOperandsInAllNamespaces = () => [boolean, (value: boolean) => void]; // This hook can be used to consume and update the showOperandsInAllNamespaces redux state export const useShowOperandsInAllNamespaces: UseShowOperandsInAllNamespaces = () => { const dispatch = useConsoleDispatch(); - const showOperandsInAllNamespaces = useConsoleSelector((state) => - state.UI.get('showOperandsInAllNamespaces'), + const showOperandsInAllNamespaces = useConsoleSelector( + (state) => state.UI.showOperandsInAllNamespaces, ); const setShowOperandsInAllNamespaces = useCallback( (value: boolean) => dispatch(UIActions.setShowOperandsInAllNamespaces(value)), diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/utils.ts b/frontend/packages/operator-lifecycle-manager/src/components/operand/utils.ts index 88b5257ea7e..920f93635bd 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/utils.ts +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/utils.ts @@ -1,5 +1,4 @@ import type { UiSchema } from '@rjsf/core'; -import * as Immutable from 'immutable'; import type { JSONSchema7 } from 'json-schema'; import * as _ from 'lodash'; import i18n from '@console/internal/i18n'; @@ -102,13 +101,13 @@ export const capabilitiesToUISchema = (capabilities: SpecCapability[] = []) => { const field = _.reduce( capabilities, - (fieldAccumulator, capability) => fieldAccumulator ?? capabilityFieldMap.get(capability), + (fieldAccumulator, capability) => fieldAccumulator ?? capabilityFieldMap[capability], undefined, ); const widget = _.reduce( capabilities, - (widgetAccumulator, capability) => widgetAccumulator ?? capabilityWidgetMap.get(capability), + (widgetAccumulator, capability) => widgetAccumulator ?? capabilityWidgetMap[capability], undefined, ); @@ -123,49 +122,46 @@ export const descriptorsToUISchema = ( descriptors: Descriptor[], jsonSchema: JSONSchema7, ) => { - const uiSchemaFromDescriptors = _.reduce( - descriptors, - (uiSchemaAccumulator, descriptor, index) => { - const schemaForDescriptor = getSchemaAtPath(jsonSchema, descriptor.path); - if (!schemaForDescriptor) { - // eslint-disable-next-line no-console - console.warn( - '[OperandForm] SpecDescriptor path references a non-existent schema property:', - descriptor.path, - ); - return uiSchemaAccumulator; - } - const capabilities = getValidCapabilitiesForSchema( - descriptor, - schemaForDescriptor, - ); - const uiSchemaPath = stringPathToUISchemaPath(descriptor.path); - const isAdvanced = capabilities.includes(SpecCapability.advanced); - const dependency = capabilities.find((capability) => - capability.startsWith(SpecCapability.fieldDependency), + const uiSchemaFromDescriptors = (descriptors ?? []).reduce((acc: UiSchema, descriptor, index) => { + const schemaForDescriptor = getSchemaAtPath(jsonSchema, descriptor.path); + if (!schemaForDescriptor) { + // eslint-disable-next-line no-console + console.warn( + '[OperandForm] SpecDescriptor path references a non-existent schema property:', + descriptor.path, ); - return uiSchemaAccumulator.withMutations((mutable) => { - if (isAdvanced) { - const advancedPropertyName = _.last(uiSchemaPath); - const pathToAdvanced = [...uiSchemaPath.slice(0, -1), 'ui:advanced']; - const currentAdvanced = mutable.getIn(pathToAdvanced) ?? Immutable.List(); - mutable.setIn(pathToAdvanced, currentAdvanced.push(advancedPropertyName)); - } - - mutable.mergeDeepIn( - uiSchemaPath, - Immutable.Map({ - ...(descriptor.description && { 'ui:description': descriptor.description }), - ...(descriptor.displayName && { 'ui:title': descriptor.displayName }), - ...(dependency && fieldDependencyCapabilityToUISchema(dependency)), - ...capabilitiesToUISchema(capabilities), - 'ui:sortOrder': index + 1, - }), - ); - }); - }, - Immutable.Map(), - ).toJS(); + return acc; + } + const capabilities = getValidCapabilitiesForSchema( + descriptor, + schemaForDescriptor, + ); + const uiSchemaPath = stringPathToUISchemaPath(descriptor.path); + const isAdvanced = capabilities.includes(SpecCapability.advanced); + const dependency = capabilities.find((capability) => + capability.startsWith(SpecCapability.fieldDependency), + ); + + if (isAdvanced) { + const advancedPropertyName = _.last(uiSchemaPath); + const pathToAdvanced = [...uiSchemaPath.slice(0, -1), 'ui:advanced']; + const currentAdvanced: string[] = _.get(acc, pathToAdvanced, []); + _.set(acc, pathToAdvanced, [...currentAdvanced, advancedPropertyName]); + } + + const descriptorUISchema = { + ...(descriptor.description && { 'ui:description': descriptor.description }), + ...(descriptor.displayName && { 'ui:title': descriptor.displayName }), + ...(dependency && fieldDependencyCapabilityToUISchema(dependency)), + ...capabilitiesToUISchema(capabilities), + 'ui:sortOrder': index + 1, + }; + + const existing = _.get(acc, uiSchemaPath, {}); + _.set(acc, uiSchemaPath, _.merge(existing, descriptorUISchema)); + + return acc; + }, {}); return _.merge(uiSchemaFromDescriptors, getJSONSchemaOrder(jsonSchema, uiSchemaFromDescriptors)); }; diff --git a/frontend/packages/topology/src/components/list-view/TopologyListView.tsx b/frontend/packages/topology/src/components/list-view/TopologyListView.tsx index b8b52265a89..a5a30615c71 100644 --- a/frontend/packages/topology/src/components/list-view/TopologyListView.tsx +++ b/frontend/packages/topology/src/components/list-view/TopologyListView.tsx @@ -323,7 +323,7 @@ const ConnectedTopologyListView: FC< ); const stateToProps = ({ UI }): TopologyListViewPropsFromState => ({ - metrics: UI.get('overview').toJS(), + metrics: UI.overview, }); const dispatchToProps = (dispatch): TopologyListViewPropsFromDispatch => ({ diff --git a/frontend/packages/topology/src/components/side-bar/components/SideBarBody.tsx b/frontend/packages/topology/src/components/side-bar/components/SideBarBody.tsx index 791d46c6ae9..df1b7814e24 100644 --- a/frontend/packages/topology/src/components/side-bar/components/SideBarBody.tsx +++ b/frontend/packages/topology/src/components/side-bar/components/SideBarBody.tsx @@ -12,9 +12,7 @@ import SideBarTabLoader from '../providers/SideBarTabLoader'; const SimpleTabNavWrapper: FC<{ tabs: Tab[] }> = ({ tabs }) => { const { t } = useTranslation('topology'); - const selectedTab = useConsoleSelector(({ UI }) => - UI.getIn(['overview', 'selectedDetailsTab']), - ); + const selectedTab = useConsoleSelector(({ UI }) => UI.overview?.selectedDetailsTab); const dispatch = useConsoleDispatch(); const queryParams = useQueryParams(); const selectTabParam = queryParams.get('selectTab'); diff --git a/frontend/packages/topology/src/filters/filter-utils.ts b/frontend/packages/topology/src/filters/filter-utils.ts index 054f0b57f4e..b3b0c8fb0d1 100644 --- a/frontend/packages/topology/src/filters/filter-utils.ts +++ b/frontend/packages/topology/src/filters/filter-utils.ts @@ -17,12 +17,12 @@ export enum NameLabelFilterValues { export const getSupportedTopologyFilters = (state: RootState): string[] => { const topology = state?.plugins?.devconsole?.topology; - return topology ? topology.get('supportedFilters') : DEFAULT_TOPOLOGY_FILTERS.map((f) => f.id); + return topology ? topology.supportedFilters : DEFAULT_TOPOLOGY_FILTERS.map((f) => f.id); }; export const getSupportedTopologyKinds = (state: RootState): { [key: string]: number } => { const topology = state?.plugins?.devconsole?.topology; - return topology ? topology.get('supportedKinds') : {}; + return topology ? topology.supportedKinds : {}; }; export const getTopologySearchQuery = () => diff --git a/frontend/packages/topology/src/redux/action.ts b/frontend/packages/topology/src/redux/action.ts index 9da3dda9046..41a44f0702c 100644 --- a/frontend/packages/topology/src/redux/action.ts +++ b/frontend/packages/topology/src/redux/action.ts @@ -20,7 +20,7 @@ export const setTopologyGraphModel = (namespace: string, graphModel: GraphModel) export const getTopologyGraphModel = (state: RootState, namespace: string): GraphModel => { const topology = state?.plugins?.devconsole?.topology; - return topology?.get('topologyGraphModel')?.[namespace]; + return topology?.topologyGraphModel?.[namespace]; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in typeof for type export diff --git a/frontend/packages/topology/src/redux/reducer.ts b/frontend/packages/topology/src/redux/reducer.ts index e39ec32973b..2545eb1cfb6 100644 --- a/frontend/packages/topology/src/redux/reducer.ts +++ b/frontend/packages/topology/src/redux/reducer.ts @@ -1,33 +1,33 @@ -import { Map } from 'immutable'; import { DEFAULT_TOPOLOGY_FILTERS } from '../filters/const'; import type { TopologyAction } from './action'; import { Actions } from './action'; -type State = Map; +type State = Record; export default (state: State, action: TopologyAction) => { if (!state) { - return Map({ + return { supportedFilters: DEFAULT_TOPOLOGY_FILTERS.map((f) => f.id), supportedKinds: {}, - }); + }; } if (action.type === Actions.supportedTopologyFilters) { - return state.set('supportedFilters', action.payload.supportedFilters); + return { ...state, supportedFilters: action.payload.supportedFilters }; } if (action.type === Actions.supportedTopologyKinds) { - return state.set('supportedKinds', action.payload.supportedKinds); + return { ...state, supportedKinds: action.payload.supportedKinds }; } if (action.type === Actions.topologyGraphModel) { - const savedGraphModels = state.get('topologyGraphModel'); - const updatedGraphModels = { - ...savedGraphModels, - [action.payload.namespace]: action.payload.graphModel, + return { + ...state, + topologyGraphModel: { + ...state.topologyGraphModel, + [action.payload.namespace]: action.payload.graphModel, + }, }; - return state.set('topologyGraphModel', updatedGraphModels); } return state; diff --git a/frontend/packages/topology/src/utils/__tests__/application-utils.spec.ts b/frontend/packages/topology/src/utils/__tests__/application-utils.spec.ts index 105e1cc2f34..d9f6eb7025f 100644 --- a/frontend/packages/topology/src/utils/__tests__/application-utils.spec.ts +++ b/frontend/packages/topology/src/utils/__tests__/application-utils.spec.ts @@ -144,11 +144,11 @@ describe('ApplicationUtils', () => { const removedModels = allArgs.map((arg) => arg[0]); expect(k8sKillMock.mock.calls.length).toEqual(6); - expect(removedModels).toContain(DeploymentConfigModel); - expect(removedModels).toContain(ImageStreamModel); - expect(removedModels).toContain(ServiceModel); - expect(removedModels).toContain(RouteModel); - expect(removedModels).toContain(BuildConfigModel); + expect(removedModels).toContainEqual(DeploymentConfigModel); + expect(removedModels).toContainEqual(ImageStreamModel); + expect(removedModels).toContainEqual(ServiceModel); + expect(removedModels).toContainEqual(RouteModel); + expect(removedModels).toContainEqual(BuildConfigModel); expect(removedModels.filter((model) => model.kind === 'Secret')).toHaveLength(1); }); @@ -160,10 +160,10 @@ describe('ApplicationUtils', () => { const removedModels = allArgs.map((arg) => arg[0]); expect(k8sKillMock.mock.calls.length).toEqual(4); - expect(removedModels).toContain(DeploymentConfigModel); - expect(removedModels).toContain(ImageStreamModel); - expect(removedModels).toContain(ServiceModel); - expect(removedModels).toContain(RouteModel); + expect(removedModels).toContainEqual(DeploymentConfigModel); + expect(removedModels).toContainEqual(ImageStreamModel); + expect(removedModels).toContainEqual(ServiceModel); + expect(removedModels).toContainEqual(RouteModel); }); it('Should delete all the specific models related to deployment config if the build config is present', async () => { @@ -173,11 +173,11 @@ describe('ApplicationUtils', () => { const allArgs = k8sKillMock.mock.calls; const removedModels = allArgs.map((arg) => arg[0]); expect(k8sKillMock.mock.calls.length).toEqual(5); - expect(removedModels).toContain(BuildConfigModel); - expect(removedModels).toContain(DeploymentConfigModel); - expect(removedModels).toContain(ImageStreamModel); - expect(removedModels).toContain(ServiceModel); - expect(removedModels).toContain(RouteModel); + expect(removedModels).toContainEqual(BuildConfigModel); + expect(removedModels).toContainEqual(DeploymentConfigModel); + expect(removedModels).toContainEqual(ImageStreamModel); + expect(removedModels).toContainEqual(ServiceModel); + expect(removedModels).toContainEqual(RouteModel); }); it('Should delete all the specific models related to daemonsets', async () => { @@ -186,7 +186,7 @@ describe('ApplicationUtils', () => { const allArgs = k8sKillMock.mock.calls; const removedModels = allArgs.map((arg) => arg[0]); expect(k8sKillMock.mock.calls.length).toEqual(1); - expect(removedModels).toContain(DaemonSetModel); + expect(removedModels).toContainEqual(DaemonSetModel); expect(removedModels.filter((model) => model.kind === 'Secret')).toHaveLength(0); }); @@ -196,7 +196,7 @@ describe('ApplicationUtils', () => { const allArgs = k8sKillMock.mock.calls; const removedModels = allArgs.map((arg) => arg[0]); expect(k8sKillMock.mock.calls.length).toEqual(1); - expect(removedModels).toContain(StatefulSetModel); + expect(removedModels).toContainEqual(StatefulSetModel); expect(removedModels.filter((model) => model.kind === 'Secret')).toHaveLength(0); }); @@ -211,8 +211,8 @@ describe('ApplicationUtils', () => { const allArgs = k8sKillMock.mock.calls; const removedModels = allArgs.map((arg) => arg[0]); expect(k8sKillMock.mock.calls.length).toEqual(2); - expect(removedModels).toContain(ImageStreamModel); - expect(removedModels).toContain(KnativeServiceModel); + expect(removedModels).toContainEqual(ImageStreamModel); + expect(removedModels).toContainEqual(KnativeServiceModel); expect(removedModels.filter((model) => model.kind === 'Secret')).toHaveLength(0); }); @@ -228,7 +228,7 @@ describe('ApplicationUtils', () => { const allArgs = k8sKillMock.mock.calls; const removedModels = allArgs.map((arg) => arg[0]); expect(k8sKillMock.mock.calls.length).toEqual(1); - expect(removedModels).toContain(CamelKameletBindingModel); + expect(removedModels).toContainEqual(CamelKameletBindingModel); expect(removedModels.filter((model) => model.kind === 'Secret')).toHaveLength(0); }); @@ -244,7 +244,7 @@ describe('ApplicationUtils', () => { const allArgs = k8sKillMock.mock.calls; const removedModels = allArgs.map((arg) => arg[0]); expect(k8sKillMock.mock.calls.length).toEqual(1); - expect(removedModels).toContain(KafkaSinkModel); + expect(removedModels).toContainEqual(KafkaSinkModel); expect(removedModels.filter((model) => model.kind === 'Secret')).toHaveLength(0); }); diff --git a/frontend/packages/topology/src/utils/useOverviewMetrics.ts b/frontend/packages/topology/src/utils/useOverviewMetrics.ts index 5016af25677..5f1d4f4fbe5 100644 --- a/frontend/packages/topology/src/utils/useOverviewMetrics.ts +++ b/frontend/packages/topology/src/utils/useOverviewMetrics.ts @@ -1,4 +1,3 @@ import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector'; -export const useOverviewMetrics = () => - useConsoleSelector((state) => state.UI.getIn(['overview', 'metrics'])); +export const useOverviewMetrics = () => useConsoleSelector((state) => state.UI.overview?.metrics); diff --git a/frontend/packages/webterminal-plugin/src/components/cloud-shell/setup/CloudShellDeveloperSetup.tsx b/frontend/packages/webterminal-plugin/src/components/cloud-shell/setup/CloudShellDeveloperSetup.tsx index 1a596226cea..42c76b91476 100644 --- a/frontend/packages/webterminal-plugin/src/components/cloud-shell/setup/CloudShellDeveloperSetup.tsx +++ b/frontend/packages/webterminal-plugin/src/components/cloud-shell/setup/CloudShellDeveloperSetup.tsx @@ -100,7 +100,7 @@ const CloudShellDeveloperSetup: FC = ({ const mapStateToProps = (state: RootState): StateProps => ({ username: getUser(state)?.username || '', - activeNamespace: state.UI.get('activeNamespace'), + activeNamespace: state.UI.activeNamespace, }); export default connect(mapStateToProps)(CloudShellDeveloperSetup); diff --git a/frontend/public/actions/__tests__/dashboards.spec.ts b/frontend/public/actions/__tests__/dashboards.spec.ts index 5fc4e7110dd..b527339a361 100644 --- a/frontend/public/actions/__tests__/dashboards.spec.ts +++ b/frontend/public/actions/__tests__/dashboards.spec.ts @@ -1,4 +1,3 @@ -import { Map as ImmutableMap } from 'immutable'; import { MIN_POLL_DELAY } from '../../components/utils/adaptive-polling'; import { RESULTS_TYPE } from '../../reducers/dashboard-results'; import { defaults } from '../../reducers/dashboards'; @@ -10,6 +9,13 @@ import { watchPrometheusQuery, } from '../dashboards'; +// Build a dashboards state with a single active watch, matching the plain-object +// shape the reducer and selectors now use. +const withActiveWatch = (type: RESULTS_TYPE, key: string, active: number) => ({ + ...defaults, + [type]: { ...defaults[type], [key]: { active } }, +}); + const testStopWatch = (stopAction, type: RESULTS_TYPE, key: string) => { expect(stopAction(key)).toEqual({ payload: { @@ -23,8 +29,8 @@ const testStopWatch = (stopAction, type: RESULTS_TYPE, key: string) => { const testStartWatch = (watchAction, type: RESULTS_TYPE, key: string) => { const getState = jest .fn() - .mockReturnValueOnce({ dashboards: ImmutableMap(defaults) }) - .mockReturnValueOnce({ dashboards: ImmutableMap(defaults).setIn([type, key, 'active'], 1) }); + .mockReturnValueOnce({ dashboards: defaults }) + .mockReturnValueOnce({ dashboards: withActiveWatch(type, key, 1) }); const dispatch = jest.fn(); watchAction(key)(dispatch, getState); @@ -47,9 +53,7 @@ const testStartWatch = (watchAction, type: RESULTS_TYPE, key: string) => { }; const testIncrementActiveWatch = (watchAction, type, key) => { - const getState = jest - .fn() - .mockReturnValue({ dashboards: ImmutableMap(defaults).setIn([type, key, 'active'], 1) }); + const getState = jest.fn().mockReturnValue({ dashboards: withActiveWatch(type, key, 1) }); const dispatch = jest.fn(); watchAction(key)(dispatch, getState); @@ -76,7 +80,7 @@ describe('dashboards-actions', () => { }); it('watchPrometheusQuery sets error if base url is not available', () => { - const getState = jest.fn().mockReturnValue({ dashboards: ImmutableMap(defaults) }); + const getState = jest.fn().mockReturnValue({ dashboards: defaults }); const dispatch = jest.fn(); watchPrometheusQuery('fooQuery')(dispatch, getState); @@ -118,10 +122,10 @@ describe('dashboards-actions', () => { const flushPromises = () => new Promise(process.nextTick); const setupWatchURL = (fetchMock: jest.Mock) => { - const activeState = ImmutableMap(defaults).setIn([RESULTS_TYPE.URL, 'testURL', 'active'], 1); + const activeState = withActiveWatch(RESULTS_TYPE.URL, 'testURL', 1); const getState = jest .fn() - .mockReturnValueOnce({ dashboards: ImmutableMap(defaults) }) + .mockReturnValueOnce({ dashboards: defaults }) .mockReturnValue({ dashboards: activeState }); const dispatch = jest.fn(); diff --git a/frontend/public/actions/dashboards.ts b/frontend/public/actions/dashboards.ts index e9da5ae2fe2..b43f1c97cbb 100644 --- a/frontend/public/actions/dashboards.ts +++ b/frontend/public/actions/dashboards.ts @@ -31,8 +31,11 @@ export const setData = (type: RESULTS_TYPE, key: string, data) => action(ActionType.SetData, { type, key, data }); export const activateWatch = (type: RESULTS_TYPE, key: string) => action(ActionType.ActivateWatch, { type, key }); -export const updateWatchTimeout = (type: RESULTS_TYPE, key: string, timeout: NodeJS.Timer) => - action(ActionType.UpdateWatchTimeout, { type, key, timeout }); +export const updateWatchTimeout = ( + type: RESULTS_TYPE, + key: string, + timeout: ReturnType, +) => action(ActionType.UpdateWatchTimeout, { type, key, timeout }); export const updateWatchInFlight = (type: RESULTS_TYPE, key: string, inFlight: boolean) => action(ActionType.UpdateWatchInFlight, { type, key, inFlight }); const setError = (type: RESULTS_TYPE, key: string, error) => @@ -57,9 +60,8 @@ export const getPrometheusQueryResponse = ( timespan?: number, ): [PrometheusResponse, any] => { const queryKey = getQueryKey(query, timespan); - const data = prometheusResults.getIn([queryKey, 'data']); - const loadError = prometheusResults.getIn([queryKey, 'loadError']); - return [data, loadError]; + const entry = prometheusResults?.[queryKey]; + return [entry?.data, entry?.loadError]; }; const fetchPeriodically: FetchPeriodically = async ( diff --git a/frontend/public/actions/ui.ts b/frontend/public/actions/ui.ts index 393c0ee83e2..31a19050b58 100644 --- a/frontend/public/actions/ui.ts +++ b/frontend/public/actions/ui.ts @@ -1,6 +1,4 @@ -/* eslint-disable no-barrel-files/no-barrel-files */ import { Base64 } from 'js-base64'; -import * as _ from 'lodash'; import type { ActionType as Action } from 'typesafe-actions'; import { action } from 'typesafe-actions'; import { @@ -26,6 +24,7 @@ import { setClusterID, setCreateProjectMessage, ActionType } from './common'; import { detectFeatures } from './features'; import { clearSSARFlags } from './flags'; +// eslint-disable-next-line no-barrel-files/no-barrel-files export type { NamespaceMetrics } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; type MetricValuesByNamespace = { @@ -56,26 +55,26 @@ export type PluginCSPViolations = { [pluginName: string]: boolean; }; -export const getActiveNamespace = (): string => store.getState().UI.get('activeNamespace'); +export const getActiveNamespace = (): string => store.getState().UI.activeNamespace; export const getActiveUserName = (): string => getUser(store.getState())?.username; export const getNamespaceMetric = (ns: K8sResourceKind, metric: string): number => { - const metrics = store.getState().UI.getIn(['metrics', 'namespace']); - return _.get(metrics, [metric, ns.metadata.name], 0); + const metrics = store.getState().UI.metrics?.namespace; + return metrics?.[metric]?.[ns.metadata.name] ?? 0; }; export const getPodMetric = (pod: PodKind, metric: string): number => { - const metrics = store.getState().UI.getIn(['metrics', 'pod']); + const metrics = store.getState().UI.metrics?.pod; return metrics?.[metric]?.[pod.metadata.namespace]?.[pod.metadata.name] ?? 0; }; export const getNodeMetric = (node: NodeKind, metric: string): number => { - const metrics = store.getState().UI.getIn(['metrics', 'node']); + const metrics = store.getState().UI.metrics?.node; return metrics?.[metric]?.[node.metadata.name] ?? 0; }; export const getPVCMetric = (pvc: K8sResourceKind, metric: string): number => { - const metrics = store.getState().UI.getIn(['metrics', 'pvc']); + const metrics = store.getState().UI.metrics?.pvc; return metrics?.[metric]?.[pvc.metadata.namespace]?.[pvc.metadata.name] ?? 0; }; diff --git a/frontend/public/components/RBAC/rules.jsx b/frontend/public/components/RBAC/rules.jsx index 78711c10f0a..a36325817d4 100644 --- a/frontend/public/components/RBAC/rules.jsx +++ b/frontend/public/components/RBAC/rules.jsx @@ -78,63 +78,61 @@ const Groups = ({ apiGroups }) => { return
{groups}
; }; -const Resources = connect(({ k8s }) => ({ allModels: k8s.getIn(['RESOURCES', 'models']) }))(({ - resources, - nonResourceURLs, - allModels, -}) => { - const { t } = useTranslation('public'); +const Resources = connect(({ k8s }) => ({ allModels: Object.values(k8s.RESOURCES?.models ?? {}) }))( + ({ resources, nonResourceURLs, allModels }) => { + const { t } = useTranslation('public'); - let allResources = []; - if (resources) { - _.each([...new Set(resources)].sort(), (r) => { - if (r === '') { - return false; - } - if (r === '*') { - allResources = [ + let allResources = []; + if (resources) { + _.each([...new Set(resources)].toSorted(), (r) => { + if (r === '') { + return false; + } + if (r === '*') { + allResources = [ + + {t('All Resources')} + , + ]; + return false; + } + const base = r.split('/')[0]; + const kind = allModels.find((model) => model.plural === base); + + allResources.push( - {t('All Resources')} + {' '} + {r} , - ]; - return false; - } - const base = r.split('/')[0]; - const kind = allModels.find((model) => model.plural === base); - - allResources.push( - - {' '} - {r} - , - ); - }); - } - - if (nonResourceURLs && nonResourceURLs.length) { - if (allResources.length) { - allResources.push(); + ); + }); } - let URLs = []; - _.each(nonResourceURLs.sort(), (r) => { - if (r === '*') { - URLs = [ + + if (nonResourceURLs && nonResourceURLs.length) { + if (allResources.length) { + allResources.push(); + } + let URLs = []; + _.each(nonResourceURLs.sort(), (r) => { + if (r === '*') { + URLs = [ +
+ {t('All Non-resource URLs')} +
, + ]; + return false; + } + URLs.push(
- {t('All Non-resource URLs')} + {r}
, - ]; - return false; - } - URLs.push( -
- {r} -
, - ); - }); - allResources.push(...URLs); - } - return
{allResources}
; -}); + ); + }); + allResources.push(...URLs); + } + return
{allResources}
; + }, +); const ResourceNames = ({ resourceNames }) => { if (!resourceNames || resourceNames.length === 0) { diff --git a/frontend/public/components/__tests__/environment.spec.tsx b/frontend/public/components/__tests__/environment.spec.tsx index eaa4b31b738..979b68f0046 100644 --- a/frontend/public/components/__tests__/environment.spec.tsx +++ b/frontend/public/components/__tests__/environment.spec.tsx @@ -1,5 +1,4 @@ import { screen, waitFor } from '@testing-library/react'; -import { fromJS, Map as ImmutableMap } from 'immutable'; import * as rbacModule from '@console/dynamic-plugin-sdk/src/app/components/utils/rbac'; import * as k8sResourceModule from '@console/dynamic-plugin-sdk/src/utils/k8s/k8s-resource'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; @@ -22,13 +21,13 @@ const k8sGetMock = k8sResourceModule.k8sGet as jest.Mock; // Provide DeploymentModel in the Redux store so checkEditAccess proceeds // past the `!model` guard and actually calls checkAccess. const initialState = { - k8s: fromJS({ + k8s: { RESOURCES: { - models: ImmutableMap().set('Deployment', DeploymentModel), + models: { Deployment: DeploymentModel }, inFlight: false, loaded: true, }, - }), + }, }; describe('EnvironmentPage', () => { diff --git a/frontend/public/components/__tests__/resource-dropdown.spec.tsx b/frontend/public/components/__tests__/resource-dropdown.spec.tsx index d1215d65c50..2ac401eda58 100644 --- a/frontend/public/components/__tests__/resource-dropdown.spec.tsx +++ b/frontend/public/components/__tests__/resource-dropdown.spec.tsx @@ -1,6 +1,5 @@ import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Map as ImmutableMap } from 'immutable'; import type { K8sModel } from '@console/dynamic-plugin-sdk/src/api/common-types'; import { getReferenceForModel } from '@console/dynamic-plugin-sdk/src/utils/k8s/k8s-ref'; import { useUserPreference } from '@console/shared/src/hooks/useUserPreference'; @@ -54,12 +53,13 @@ const makeModel = ( ...overrides, }); -const buildModelsMap = (models: K8sKind[]): ImmutableMap => - ImmutableMap().withMutations((map) => { - models.forEach((m) => { - map.set(getReferenceForModel(m), m); - }); +const buildModelsMap = (models: K8sKind[]): Record => { + const map: Record = {}; + models.forEach((m) => { + map[getReferenceForModel(m)] = m; }); + return map; +}; const defaultProps = { selected: [] as string[], diff --git a/frontend/public/components/api-explorer.tsx b/frontend/public/components/api-explorer.tsx index 6e477186ef2..8e6c141b601 100644 --- a/frontend/public/components/api-explorer.tsx +++ b/frontend/public/components/api-explorer.tsx @@ -20,7 +20,6 @@ import { RhUiFilterIcon } from '@patternfly/react-icons'; import { InnerScrollContainer, Tbody, Tr, Td } from '@patternfly/react-table'; import * as fuzzy from 'fuzzysearch'; import i18next from 'i18next'; -import type { Map as ImmutableMap } from 'immutable'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; @@ -74,7 +73,7 @@ import { ScrollToTopOnMount } from './utils/scroll-to-top-on-mount'; import { LoadError, LoadingBox } from './utils/status-box'; const mapStateToProps = (state: RootState): APIResourceLinkStateProps => ({ - activeNamespace: state.UI.get('activeNamespace'), + activeNamespace: state.UI.activeNamespace, }); const getAPIResourceLink = (activeNamespace: string, model: K8sKind) => { @@ -142,8 +141,8 @@ const BodyEmpty: FC<{ label: string; colSpan: number }> = ({ label, colSpan }) = const APIResourcesList: FC = () => { const { setQueryArgument, removeQueryArgument } = useQueryParamsMutator(); const location = useLocation(); - const models: ImmutableMap = useConsoleSelector((state) => - state.k8s.getIn(['RESOURCES', 'models']), + const models: Record = useConsoleSelector( + (state) => state.k8s.RESOURCES?.models, ); const ALL = '#all#'; const GROUP_PARAM = 'g'; @@ -203,7 +202,8 @@ const APIResourcesList: FC = () => { const navigate = useNavigate(); // group options - const groups: Set = models.reduce( + const modelValues = Object.values(models ?? {}); + const groups: Set = modelValues.reduce( (result: Set, { apiGroup }) => (apiGroup ? result.add(apiGroup) : result), new Set(), ); @@ -231,7 +231,7 @@ const APIResourcesList: FC = () => { }; // version options - const versions: Set = models.reduce( + const versions: Set = modelValues.reduce( (result: Set, { apiVersion }) => result.add(apiVersion), new Set(), ); @@ -257,7 +257,7 @@ const APIResourcesList: FC = () => { const scopeSpacer = new Set(['cluster']); // filter by group, version, or text - const filteredResources = models.filter(({ kind, apiGroup, apiVersion, namespaced }) => { + const filteredResources = modelValues.filter(({ kind, apiGroup, apiVersion, namespaced }) => { if (groupFilter !== ALL && (apiGroup || '') !== groupFilter) { return false; } @@ -300,7 +300,7 @@ const APIResourcesList: FC = () => { }; const sortedResources = useMemo(() => { - const sorted = [...filteredResources.toArray()]; + const sorted = [...filteredResources]; // Check if user has manually sorted (sortBy params exist in URL) const hasUserSort = sortByParam !== '0' || orderByParam !== 'asc'; @@ -440,7 +440,13 @@ const APIResourcesList: FC = () => { = ({ customData: { kindObj } }) const APIResourceInstances: FC = ({ customData: { kindObj, namespace } }) => { const resourceListPageExtensions = useExtensions(isResourceListPage); - const componentLoader = getResourceListPages(resourceListPageExtensions).get( - referenceForModel(kindObj), - () => Promise.resolve(DefaultPage), - ); + const componentLoader = + getResourceListPages(resourceListPageExtensions).get(referenceForModel(kindObj)) ?? + (() => Promise.resolve(DefaultPage)); const ns = kindObj.namespaced ? namespace : undefined; return ( @@ -950,7 +955,7 @@ const InnerAPIResourcePage = (props) => { const kind: string = props.kind || params?.plural; const kindObj = getK8sModel(props.k8s, kind); - const kindsInFlight = props.k8s.getIn(['RESOURCES', 'inFlight']); + const kindsInFlight = props.k8s.RESOURCES?.inFlight; const namespace = kindObj?.namespaced ? params.ns : undefined; const { t } = useTranslation('public'); diff --git a/frontend/public/components/cluster-settings/global-config.tsx b/frontend/public/components/cluster-settings/global-config.tsx index d850400820a..be6c906aabd 100644 --- a/frontend/public/components/cluster-settings/global-config.tsx +++ b/frontend/public/components/cluster-settings/global-config.tsx @@ -73,9 +73,8 @@ const useConfigResources = () => { clusterOperatorConfigResources: K8sKind[]; configResources: K8sKind[]; }>(({ k8s }) => ({ - clusterOperatorConfigResources: - k8s.getIn(['RESOURCES', 'clusterOperatorConfigResources']) ?? [], - configResources: k8s.getIn(['RESOURCES', 'configResources']) ?? [], + clusterOperatorConfigResources: k8s.RESOURCES?.clusterOperatorConfigResources ?? [], + configResources: k8s.RESOURCES?.configResources ?? [], })); const canClusterUpgrade = useCanClusterUpgrade(); diff --git a/frontend/public/components/create-yaml.tsx b/frontend/public/components/create-yaml.tsx index 71b20974484..4986f214053 100644 --- a/frontend/public/components/create-yaml.tsx +++ b/frontend/public/components/create-yaml.tsx @@ -42,8 +42,8 @@ export const CreateYAMLInner: FC = ({ } const resolvedTemplate = template || - yamlTemplates.getIn([referenceForModel(kindObj), 'default']) || - yamlTemplates.getIn(['DEFAULT', 'default']); + yamlTemplates?.[referenceForModel(kindObj)]?.default || + yamlTemplates?.DEFAULT?.default; if (!resolvedTemplate) { return {}; } @@ -55,8 +55,7 @@ export const CreateYAMLInner: FC = ({ const { metadata, spec } = parsed; const { crd, kind, namespaced } = kindObj; - const isDefaultTemplate = - crd && resolvedTemplate === yamlTemplates.getIn(['DEFAULT', 'default']); + const isDefaultTemplate = crd && resolvedTemplate === yamlTemplates?.DEFAULT?.default; return { ...parsed, kind, diff --git a/frontend/public/components/custom-resource-definition.tsx b/frontend/public/components/custom-resource-definition.tsx index 9658c12fc89..db7090ea40a 100644 --- a/frontend/public/components/custom-resource-definition.tsx +++ b/frontend/public/components/custom-resource-definition.tsx @@ -191,9 +191,9 @@ const Details: FC<{ obj: CustomResourceDefinitionKind }> = ({ obj: crd }) => { const Instances: FC = ({ obj, namespace }) => { const resourceListPageExtensions = useExtensions(isResourceListPage); const crdKind = referenceForCRD(obj); - const componentLoader = getResourceListPages(resourceListPageExtensions).get(crdKind, () => - Promise.resolve(DefaultPage), - ); + const componentLoader = + getResourceListPages(resourceListPageExtensions).get(crdKind) ?? + (() => Promise.resolve(DefaultPage)); return ( { }; const mapStateToProps = ({ k8s }) => ({ - models: k8s.getIn(['RESOURCES', 'models']), + models: k8s.RESOURCES?.models, }); const OngoingActivityComponent: FC = ({ models }) => { @@ -73,7 +72,7 @@ const OngoingActivityComponent: FC = ({ models }) => { ); const resourceActivities = useMemo( - () => resourceActivityExtensions.filter((e) => !!models.get(e.properties.k8sResource.kind)), + () => resourceActivityExtensions.filter((e) => !!models?.[e.properties.k8sResource.kind]), [resourceActivityExtensions, models], ); @@ -128,13 +127,13 @@ const OngoingActivityComponent: FC = ({ models }) => { prometheusActivities .filter((a) => { const queryResults = a.properties.queries.map( - (q) => prometheusResults.getIn([q, 'data']) as PrometheusResponse, + (q) => prometheusResults?.[q]?.data as PrometheusResponse, ); return a.properties.isActivity(queryResults); }) .map((a) => { const queryResults = a.properties.queries.map( - (q) => prometheusResults.getIn([q, 'data']) as PrometheusResponse, + (q) => prometheusResults?.[q]?.data as PrometheusResponse, ); return { component: a.properties.component, @@ -157,7 +156,7 @@ const OngoingActivityComponent: FC = ({ models }) => { () => prometheusActivities.every((a) => a.properties.queries.every( - (q) => prometheusResults.getIn([q, 'data']) || prometheusResults.getIn([q, 'loadError']), + (q) => prometheusResults?.[q]?.data || prometheusResults?.[q]?.loadError, ), ), [prometheusActivities, prometheusResults], @@ -191,5 +190,5 @@ export const ActivityCard = memo(() => { }); type OngoingActivityProps = { - models: ImmutableMap; + models: Record; }; diff --git a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/health-item.tsx b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/health-item.tsx index 6269d0f319b..bf6eadac165 100644 --- a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/health-item.tsx +++ b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/health-item.tsx @@ -1,7 +1,6 @@ import type { ComponentType, FC } from 'react'; import { useEffect, useContext, useMemo } from 'react'; import { Stack, StackItem } from '@patternfly/react-core'; -import type { Map as ImmutableMap } from 'immutable'; import { useTranslation } from 'react-i18next'; import type { ResolvedExtension, @@ -156,14 +155,13 @@ export const URLHealthItem: FC = ({ subsystem, models }) => [subsystem.url, subsystem.fetch], ); const { urlResults } = useDashboardResources({ urls }); - const modelExists = - subsystem.additionalResource && !!models.get(subsystem.additionalResource.kind); + const modelExists = subsystem.additionalResource && !!models?.[subsystem.additionalResource.kind]; const [k8sData, k8sLoaded, k8sLoadError] = useK8sWatchResource( modelExists ? subsystem.additionalResource : null, ); - const healthResult = urlResults.getIn([subsystem.url, 'data']); - const healthResultError = urlResults.getIn([subsystem.url, 'loadError']); + const healthResult = urlResults?.[subsystem.url]?.data; + const healthResultError = urlResults?.[subsystem.url]?.loadError; const k8sResult = modelExists ? { data: k8sData, loaded: k8sLoaded, loadError: k8sLoadError } @@ -204,8 +202,7 @@ export const PrometheusHealthItem: FC = ({ subsystem, ); const { prometheusResults } = useDashboardResources({ prometheusQueries }); - const modelExists = - subsystem.additionalResource && !!models.get(subsystem.additionalResource.kind); + const modelExists = subsystem.additionalResource && !!models?.[subsystem.additionalResource.kind]; const [k8sData, k8sLoaded, k8sLoadError] = useK8sWatchResource( modelExists ? subsystem.additionalResource : null, ); @@ -295,12 +292,12 @@ type OperatorHealthItemProps = { type URLHealthItemProps = { subsystem: ResolvedExtension>['properties']; - models: ImmutableMap; + models: Record; }; type PrometheusHealthItemProps = { subsystem: ResolvedExtension['properties']; - models: ImmutableMap; + models: Record; }; type ResourceHealthItemProps = { diff --git a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/status-card.tsx b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/status-card.tsx index 5025c736d0b..a57e105301e 100644 --- a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/status-card.tsx +++ b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/status-card.tsx @@ -1,7 +1,6 @@ import type { FC, ReactNode } from 'react'; import { useMemo } from 'react'; import { Gallery, GalleryItem, Card, CardHeader, CardTitle } from '@patternfly/react-core'; -import type { Map as ImmutableMap } from 'immutable'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; @@ -55,7 +54,7 @@ const filterSubsystems = ( subsystems: ( DashboardsOverviewHealthSubsystem | ResolvedExtension )[], - k8sModels: ImmutableMap, + k8sModels: Record, ) => subsystems.filter((s) => { if ( @@ -67,7 +66,7 @@ const filterSubsystems = ( | ResolvedExtension; return subsystem.properties.additionalResource && !subsystem.properties.additionalResource.optional - ? !!k8sModels.get(subsystem.properties.additionalResource.kind) + ? !!k8sModels?.[subsystem.properties.additionalResource.kind] : true; } return true; @@ -122,7 +121,7 @@ export const DashboardNamespacedAlerts: FC = ({ }; const mapStateToProps = (state: RootState) => ({ - k8sModels: state.k8s.getIn(['RESOURCES', 'models']), + k8sModels: state.k8s.RESOURCES?.models, }); export const StatusCard = connect(mapStateToProps)(({ k8sModels }) => { const [subsystemExtensions] = useResolvedExtensions( @@ -220,7 +219,7 @@ export const StatusCard = connect(mapStateToProps)(({ k8sModels }); type StatusCardProps = { - k8sModels: ImmutableMap; + k8sModels: Record; }; type DashboardAlertsProps = { diff --git a/frontend/public/components/dashboard/dashboards-page/dashboards.tsx b/frontend/public/components/dashboard/dashboards-page/dashboards.tsx index 1369e92bffd..ed518a6f710 100644 --- a/frontend/public/components/dashboard/dashboards-page/dashboards.tsx +++ b/frontend/public/components/dashboard/dashboards-page/dashboards.tsx @@ -102,8 +102,8 @@ const InnerDashboardsPage: FC = ({ kindsInFlight, k8sModels }; const mapStateToProps = (state: RootState) => ({ - kindsInFlight: state.k8s.getIn(['RESOURCES', 'inFlight']), - k8sModelsLoaded: state.k8s.getIn(['RESOURCES', 'loaded']), + kindsInFlight: state.k8s.RESOURCES?.inFlight, + k8sModelsLoaded: state.k8s.RESOURCES?.loaded, }); export const DashboardsPage = connect(mapStateToProps)(InnerDashboardsPage); diff --git a/frontend/public/components/dashboard/project-dashboard/activity-card.tsx b/frontend/public/components/dashboard/project-dashboard/activity-card.tsx index a56f10dfc4f..444bfa64727 100644 --- a/frontend/public/components/dashboard/project-dashboard/activity-card.tsx +++ b/frontend/public/components/dashboard/project-dashboard/activity-card.tsx @@ -1,7 +1,6 @@ import type { FC } from 'react'; import { useEffect, useMemo, useContext, memo } from 'react'; import { Card, CardFooter, CardHeader, CardTitle, Divider } from '@patternfly/react-core'; -import type { Map as ImmutableMap } from 'immutable'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; @@ -65,7 +64,7 @@ const RecentEvent: FC<{ projectName: string; viewEvents: string }> = ({ }; const mapStateToProps = (state: RootState): OngoingActivityReduxProps => ({ - models: state.k8s.getIn(['RESOURCES', 'models']) as ImmutableMap, + models: state.k8s.RESOURCES?.models as Record, }); const OngoingActivityComponent: FC = ({ projectName, models }) => { @@ -78,7 +77,7 @@ const OngoingActivityComponent: FC = ({ projectName, model const resourceActivities = useMemo( () => resourceActivityExtensions.filter((e) => { - const model = models.get(e.properties.k8sResource.kind); + const model = models?.[e.properties.k8sResource.kind]; return model && model.namespaced; }), [resourceActivityExtensions, models], @@ -134,7 +133,7 @@ const OngoingActivityComponent: FC = ({ projectName, model return ( ); @@ -163,7 +162,7 @@ export const ActivityCard = memo(() => { }); type OngoingActivityReduxProps = { - models: ImmutableMap; + models: Record; }; type OngoingActivityProps = OngoingActivityReduxProps & { diff --git a/frontend/public/components/edit-yaml.tsx b/frontend/public/components/edit-yaml.tsx index e761e195230..904ccc0a866 100644 --- a/frontend/public/components/edit-yaml.tsx +++ b/frontend/public/components/edit-yaml.tsx @@ -72,7 +72,7 @@ const generateObjToLoad = ( namespace = 'default', ) => { const sampleObj: K8sResourceKind = safeLoad( - yaml || getYAMLTemplates(templateExtensions).getIn([kind, id]), + yaml || getYAMLTemplates(templateExtensions)?.[kind]?.[id], ) as K8sResourceKind; if (_.has(sampleObj.metadata, 'namespace')) { sampleObj.metadata.namespace = namespace; @@ -83,7 +83,7 @@ const generateObjToLoad = ( const stateToProps = (state: RootState) => ({ activeNamespace: getActiveNamespace(state), impersonate: getImpersonate(state), - models: state.k8s.getIn(['RESOURCES', 'models']) as Map, + models: state.k8s.RESOURCES?.models as Record, }); export interface EditYAMLProps { @@ -220,7 +220,7 @@ const EditYAMLInner: FC = (props) => { if (_.isEmpty(obj) || !models) { return null; } - return models.get(referenceFor(obj)) || models.get(obj.kind); + return models[referenceFor(obj)] || models[obj.kind]; }, [models], ); diff --git a/frontend/public/components/environment.tsx b/frontend/public/components/environment.tsx index ff17c56cb53..6f47040abbd 100644 --- a/frontend/public/components/environment.tsx +++ b/frontend/public/components/environment.tsx @@ -364,8 +364,7 @@ export const EnvironmentPage: FC = (props) => { const model = useConsoleSelector( (state) => - state.k8s.getIn(['RESOURCES', 'models', referenceFor(obj)]) || - state.k8s.getIn(['RESOURCES', 'models', obj?.kind]), + state.k8s.RESOURCES?.models?.[referenceFor(obj)] || state.k8s.RESOURCES?.models?.[obj?.kind], ); const impersonate = useConsoleSelector((state) => getImpersonate(state)); diff --git a/frontend/public/components/factory/list-page.tsx b/frontend/public/components/factory/list-page.tsx index 0050c5a1e54..68c68038dcd 100644 --- a/frontend/public/components/factory/list-page.tsx +++ b/frontend/public/components/factory/list-page.tsx @@ -111,9 +111,9 @@ export const ListPageWrapper: FC = (props) => { return undefined; } return memoizedIds.reduce((acc, id) => { - const idFilters = state.k8s.getIn([id, 'filters']); + const idFilters = state.k8s[id]?.filters; if (idFilters) { - idFilters.forEach((value, key) => { + Object.entries(idFilters).forEach(([key, value]) => { acc[key] = value; }); } diff --git a/frontend/public/components/factory/table-data-hook.ts b/frontend/public/components/factory/table-data-hook.ts index 807cff7a9f4..ad2cc37c40b 100644 --- a/frontend/public/components/factory/table-data-hook.ts +++ b/frontend/public/components/factory/table-data-hook.ts @@ -104,12 +104,8 @@ export const useTableData = ({ const sortSelector = useMemo( () => tableSelectorCreator( - (state: RootState) => state.UI.getIn(['listSorts', listId]), - (sortsState: any) => [ - sortsState?.get('field'), - sortsState?.get('func'), - sortsState?.get('orderBy'), - ], + (state: RootState) => state.UI.listSorts?.[listId], + (sortsState: any) => [sortsState?.field, sortsState?.func, sortsState?.orderBy], ), [tableSelectorCreator, listId], ); diff --git a/frontend/public/components/graphs/prometheus-graph.tsx b/frontend/public/components/graphs/prometheus-graph.tsx index 305ea304b6a..99c0b866066 100644 --- a/frontend/public/components/graphs/prometheus-graph.tsx +++ b/frontend/public/components/graphs/prometheus-graph.tsx @@ -13,7 +13,7 @@ import type { RootState } from '../../redux'; const mapStateToProps = (state: RootState) => ({ canAccessMonitoring: - !!state[featureReducerName].get(FLAGS.CAN_GET_NS) && !!window.SERVER_FLAGS.prometheusBaseURL, + !!state[featureReducerName][FLAGS.CAN_GET_NS] && !!window.SERVER_FLAGS.prometheusBaseURL, namespace: getActiveNamespace(state), }); diff --git a/frontend/public/components/list-pages.ts b/frontend/public/components/list-pages.ts index 16d1b4ed0a8..0e6114fb5b1 100644 --- a/frontend/public/components/list-pages.ts +++ b/frontend/public/components/list-pages.ts @@ -1,4 +1,3 @@ -import { Map as ImmutableMap } from 'immutable'; import { PodDisruptionBudgetModel } from '@console/app/src/models'; import type { ResourceListPage } from '@console/dynamic-plugin-sdk'; import { @@ -55,7 +54,7 @@ type ResourceMapKey = GroupVersionKind | string; type ResourceMapValue = () => Promise>; const addDynamicResourcePage = ( - map: ImmutableMap, + map: Map, page: ResourceListPage, ) => { const key = referenceForExtensionModel(page.properties.model); @@ -64,7 +63,7 @@ const addDynamicResourcePage = ( } }; -const baseListPages = ImmutableMap() +const baseListPages: Map = new Map() .set(referenceForModel(ConfigMapModel), () => import('./configmap' /* webpackChunkName: "configmap" */).then((m) => m.ConfigMapsPage), ) @@ -253,11 +252,8 @@ const baseListPages = ImmutableMap() ).then((m) => m.VolumeSnapshotClassPage), ); -export const getResourceListPages = (pluginPages: ResourceListPage[] = []) => - ImmutableMap() - .merge(baseListPages) - .withMutations((map) => { - pluginPages.forEach((page) => { - addDynamicResourcePage(map, page); - }); - }); +export const getResourceListPages = (pluginPages: ResourceListPage[] = []) => { + const map = new Map(baseListPages); + pluginPages.forEach((page) => addDynamicResourcePage(map, page)); + return map; +}; diff --git a/frontend/public/components/masthead/masthead-toolbar.tsx b/frontend/public/components/masthead/masthead-toolbar.tsx index 14ad935b147..a14466b0af5 100644 --- a/frontend/public/components/masthead/masthead-toolbar.tsx +++ b/frontend/public/components/masthead/masthead-toolbar.tsx @@ -177,9 +177,9 @@ const MastheadToolbarContents: FC = ({ ); const { clusterID, alertCount, canAccessNS, impersonate } = useConsoleSelector( (state) => ({ - clusterID: state.UI.get('clusterID'), - alertCount: state.observe.getIn(['alertCount']), - canAccessNS: !!state[featureReducerName].get(FLAGS.CAN_GET_NS), + clusterID: state.UI.clusterID, + alertCount: state.observe.alertCount, + canAccessNS: !!state[featureReducerName][FLAGS.CAN_GET_NS], impersonate: getImpersonate(state), }), shallowEqual, diff --git a/frontend/public/components/namespace-bar.tsx b/frontend/public/components/namespace-bar.tsx index b6b6aff27cc..b5a75efe39e 100644 --- a/frontend/public/components/namespace-bar.tsx +++ b/frontend/public/components/namespace-bar.tsx @@ -129,8 +129,8 @@ export const NamespaceBar: FC = children, hideProjects = false, }) => { - const useProjects = useConsoleSelector(({ k8s }) => - k8s.hasIn(['RESOURCES', 'models', ProjectModel.kind]), + const useProjects = useConsoleSelector( + ({ k8s }) => !!k8s.RESOURCES?.models?.[ProjectModel.kind], ); const [namespaces, loaded, loadError] = useK8sWatchResource( diff --git a/frontend/public/components/namespace.jsx b/frontend/public/components/namespace.jsx index bee0a6f0266..64ffcd4be0d 100644 --- a/frontend/public/components/namespace.jsx +++ b/frontend/public/components/namespace.jsx @@ -404,7 +404,7 @@ const NamespacesList = (props) => { undefined, true, ); - const namespaceMetrics = useConsoleSelector(({ UI }) => UI.getIn(['metrics', 'namespace'])); + const namespaceMetrics = useConsoleSelector(({ UI }) => UI.metrics?.namespace); // TODO Utilize usePoll hook useEffect(() => { @@ -786,7 +786,7 @@ const ProjectList = (props) => { const showMetrics = isPrometheusAvailable; const showActions = true; const { columns, resetAllColumnWidths } = useProjectsColumns({ showMetrics, showActions }); - const namespaceMetrics = useConsoleSelector(({ UI }) => UI.getIn(['metrics', 'namespace'])); + const namespaceMetrics = useConsoleSelector(({ UI }) => UI.metrics?.namespace); const namespaces = useMemo( () => (props.data || []).map((project) => project.metadata?.name).filter(Boolean), diff --git a/frontend/public/components/persistent-volume-claim.tsx b/frontend/public/components/persistent-volume-claim.tsx index 682674b7155..b7cc727e4e9 100644 --- a/frontend/public/components/persistent-volume-claim.tsx +++ b/frontend/public/components/persistent-volume-claim.tsx @@ -532,7 +532,7 @@ const PersistentVolumeClaimList: FC = ({ }) => { const { t } = useTranslation('public'); const { columns, resetAllColumnWidths } = usePersistentVolumeClaimColumns(); - const pvcMetrics = useConsoleSelector(({ UI }) => UI.getIn(['metrics', 'pvc'])); + const pvcMetrics = useConsoleSelector(({ UI }) => UI.metrics?.pvc); const getDataViewRows = useMemo(() => getDataViewRowsCreator(t, pvcMetrics), [t, pvcMetrics]); diff --git a/frontend/public/components/pod-list.tsx b/frontend/public/components/pod-list.tsx index 3ec8ab60e92..721143b7de3 100644 --- a/frontend/public/components/pod-list.tsx +++ b/frontend/public/components/pod-list.tsx @@ -484,9 +484,7 @@ export const PodList: FC = ({ const { t } = useTranslation('public'); const { columns, resetAllColumnWidths } = usePodsColumns(showNodes); - const podMetrics = useConsoleSelector(({ UI }) => - UI.getIn(['metrics', 'pod']), - ); + const podMetrics = useConsoleSelector(({ UI }) => UI.metrics?.pod); const columnManagementID = referenceForModel(PodModel); diff --git a/frontend/public/components/resource-dropdown.tsx b/frontend/public/components/resource-dropdown.tsx index e055dfcb8c6..3a79c6703a2 100644 --- a/frontend/public/components/resource-dropdown.tsx +++ b/frontend/public/components/resource-dropdown.tsx @@ -17,8 +17,6 @@ import { } from '@patternfly/react-core'; import { RhUiCloseIcon } from '@patternfly/react-icons'; import { css } from '@patternfly/react-styles'; -import type { Map as ImmutableMap } from 'immutable'; -import { Set as ImmutableSet } from 'immutable'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; @@ -32,12 +30,12 @@ const RECENT_SEARCH_ITEMS = 5; const MAX_VISIBLE_ITEMS = 250; // Blocklist known duplicate resources. -const blocklistGroups = ImmutableSet([ +const blocklistGroups = new Set([ // Prefer rbac.authorization.k8s.io/v1, which has the same resources. 'authorization.openshift.io', ]); -const blocklistResources = ImmutableSet([ +const blocklistResources = new Set([ // Prefer core/v1 'events.k8s.io/v1beta1.Event', ]); @@ -65,15 +63,18 @@ export const InnerResourceListDropdown: FC = (props) const textInputRef = useRef(); const resources = useMemo(() => { + if (!allModels) { + return []; + } // Pre-compute which group+kind combinations have a visible preferred version (O(n)) const preferredGroupKinds = new Set(); - allModels.forEach((m) => { + Object.values(allModels).forEach((m) => { if (groupToVersionMap?.[m.apiGroup]?.preferredVersion === m.apiVersion && isVisible(m)) { preferredGroupKinds.add(`${m.kind}~${m.apiGroup}`); } }); - return allModels + return Object.values(allModels) .filter((m) => { if (!isVisible(m)) { return false; @@ -91,13 +92,12 @@ export const InnerResourceListDropdown: FC = (props) return true; }) - .toOrderedMap() - .sortBy(({ kind, apiGroup }) => `${kind} ${apiGroup}`); + .sort((a, b) => `${a.kind} ${a.apiGroup}`.localeCompare(`${b.kind} ${b.apiGroup}`)); }, [allModels, groupToVersionMap]); const initialSelectOptions = useMemo( () => - resources.toArray().map((resource) => { + resources.map((resource) => { const reference = referenceForModel(resource); return { value: reference, @@ -157,8 +157,8 @@ export const InnerResourceListDropdown: FC = (props) }, [resources]); // Track duplicate names so we know when to show the group. - const kinds = useMemo(() => resources.groupBy((m) => m.kind), [resources]); - const isDup = (kind) => kinds.get(kind).size > 1; + const kinds = useMemo(() => _.groupBy(resources, (m) => m.kind), [resources]); + const isDup = (kind) => kinds[kind]?.length > 1; const visibleSelectOptions = selectOptions.slice(0, MAX_VISIBLE_ITEMS); const items = visibleSelectOptions.map((option: SelectOptionProps, index) => { @@ -510,8 +510,8 @@ interface ExtendedSelectOptionProps extends SelectOptionProps { } const resourceListDropdownStateToProps = ({ k8s }) => ({ - allModels: k8s.getIn(['RESOURCES', 'models']), - groupToVersionMap: k8s.getIn(['RESOURCES', 'groupToVersionMap']), + allModels: k8s.RESOURCES?.models, + groupToVersionMap: k8s.RESOURCES?.groupToVersionMap, }); export const ResourceListDropdown = connect( @@ -527,6 +527,6 @@ export type ResourceListDropdownProps = ResourceListDropdownStateToProps & { }; type ResourceListDropdownStateToProps = { - allModels: ImmutableMap; + allModels: Record; groupToVersionMap: DiscoveryResources['groupVersionMap']; }; diff --git a/frontend/public/components/resource-list.tsx b/frontend/public/components/resource-list.tsx index 00522d65a1c..78cbb7b8115 100644 --- a/frontend/public/components/resource-list.tsx +++ b/frontend/public/components/resource-list.tsx @@ -52,9 +52,9 @@ const InnerResourceListPage = connectToPlural( ); } const ref = referenceForModel(kindObj); - const componentLoader = getResourceListPages(resourceListPageExtensions).get(ref, () => - Promise.resolve(DefaultPage), - ); + const componentLoader = + getResourceListPages(resourceListPageExtensions).get(ref) ?? + (() => Promise.resolve(DefaultPage)); return (
diff --git a/frontend/public/components/resource-pages.ts b/frontend/public/components/resource-pages.ts index 37a39497642..51e02b68259 100644 --- a/frontend/public/components/resource-pages.ts +++ b/frontend/public/components/resource-pages.ts @@ -1,4 +1,3 @@ -import { Map as ImmutableMap } from 'immutable'; import { PodDisruptionBudgetModel } from '@console/app/src/models'; import type { ResourceDetailsPage } from '@console/dynamic-plugin-sdk'; import { @@ -55,7 +54,7 @@ import type { GroupVersionKind } from '../module/k8s'; import { referenceForModel, referenceForExtensionModel } from '../module/k8s'; const addDynamicResourcePage = ( - map: ImmutableMap, + map: Map, page: ResourceDetailsPage, ) => { const key = referenceForExtensionModel(page.properties.model); @@ -67,7 +66,7 @@ const addDynamicResourcePage = ( type ResourceMapKey = GroupVersionKind | string; type ResourceMapValue = () => Promise>; -const baseDetailsPages = ImmutableMap() +const baseDetailsPages: Map = new Map() .set(referenceForModel(ConfigMapModel), () => import('./configmap' /* webpackChunkName: "configmap" */).then((m) => m.ConfigMapsDetailsPage), ) @@ -286,11 +285,8 @@ const baseDetailsPages = ImmutableMap() ).then((m) => m.VolumeSnapshotClassDetailsPage), ); -export const getResourceDetailsPages = (pluginPages: ResourceDetailsPage[] = []) => - ImmutableMap() - .merge(baseDetailsPages) - .withMutations((map) => { - pluginPages.forEach((page) => { - addDynamicResourcePage(map, page); - }); - }); +export const getResourceDetailsPages = (pluginPages: ResourceDetailsPage[] = []) => { + const map = new Map(baseDetailsPages); + pluginPages.forEach((page) => addDynamicResourcePage(map, page)); + return map; +}; diff --git a/frontend/public/components/search.tsx b/frontend/public/components/search.tsx index 2f07e0ae9d5..5419a9e81d8 100644 --- a/frontend/public/components/search.tsx +++ b/frontend/public/components/search.tsx @@ -61,10 +61,9 @@ const ResourceList = memo(({ kind, mock, namespace, selector, return ; } - const componentLoader = getResourceListPages(resourceListPageExtensions).get( - referenceForModel(kindObj), - () => Promise.resolve(DefaultPage), - ); + const componentLoader = + getResourceListPages(resourceListPageExtensions).get(referenceForModel(kindObj)) ?? + (() => Promise.resolve(DefaultPage)); const ns = kindObj.namespaced ? namespace : undefined; return ( diff --git a/frontend/public/components/start-guide.tsx b/frontend/public/components/start-guide.tsx index 5b425e082e5..a785992c100 100644 --- a/frontend/public/components/start-guide.tsx +++ b/frontend/public/components/start-guide.tsx @@ -23,7 +23,7 @@ export const OpenShiftGettingStarted: FC = () => { const canCreateNamespace = useFlag(FLAGS.CAN_CREATE_NS); const canCreateProject = useFlag(FLAGS.CAN_CREATE_PROJECT); const canCreate = canCreateNamespace || canCreateProject; - const createProjectMessage = useConsoleSelector(({ UI }) => UI.get('createProjectMessage')); + const createProjectMessage = useConsoleSelector(({ UI }) => UI.createProjectMessage); const createNamespaceOrProjectModal = useCreateNamespaceOrProjectModal(); const onClickCreate = () => createNamespaceOrProjectModal({ diff --git a/frontend/public/components/utils/service-level.tsx b/frontend/public/components/utils/service-level.tsx index f430e917f16..7c68f6da23d 100644 --- a/frontend/public/components/utils/service-level.tsx +++ b/frontend/public/components/utils/service-level.tsx @@ -176,7 +176,7 @@ const useGetServiceLevel = ( loadingServiceLevel: boolean; } => { const { level, daysRemaining, clusterID, trialDateEnd, hasSecretAccess } = useConsoleSelector( - ({ UI }) => UI.get('serviceLevel'), + ({ UI }) => UI.serviceLevel, ); const [loadingSecret, loadingServiceLevel, loadServiceLevel] = useLoadServiceLevel(); diff --git a/frontend/public/kinds.ts b/frontend/public/kinds.ts index c3aa5fe38ee..b5a6184cdef 100644 --- a/frontend/public/kinds.ts +++ b/frontend/public/kinds.ts @@ -9,7 +9,7 @@ export const connectToModel = connect( const kind: string = props.kind || props.match?.params?.plural || props.params?.plural; return { kindObj: getK8sModel(k8s, kind), - kindsInFlight: k8s.getIn(['RESOURCES', 'inFlight']), + kindsInFlight: k8s.RESOURCES?.inFlight, } as any; }, ); @@ -39,7 +39,7 @@ export const connectToPlural = connect( let kindObj: K8sKind = null; if (groupVersionKind) { const [group, version, kind] = groupVersionKind; - kindObj = allModels().find( + kindObj = Object.values(allModels()).find( (model) => (model.apiGroup ?? 'core') === group && model.apiVersion === version && @@ -50,13 +50,13 @@ export const connectToPlural = connect( kindObj = getK8sModel(k8s, plural); } } else { - kindObj = allModels().find( + kindObj = Object.values(allModels()).find( (model) => model.plural === plural && (!model.crd || model.legacyPluralURL), ); } const modelRef = isGroupVersionKind(plural) ? plural : kindObj?.kind; - return { kindObj, modelRef, kindsInFlight: k8s.getIn(['RESOURCES', 'inFlight']) }; + return { kindObj, modelRef, kindsInFlight: k8s.RESOURCES?.inFlight }; }, ); diff --git a/frontend/public/models/yaml-templates.ts b/frontend/public/models/yaml-templates.ts index b900fa0fd09..92df8bdedf7 100644 --- a/frontend/public/models/yaml-templates.ts +++ b/frontend/public/models/yaml-templates.ts @@ -1,4 +1,3 @@ -import { Map as ImmutableMap } from 'immutable'; import { PodDisruptionBudgetModel } from '@console/app/src/models'; import * as appModels from '@console/app/src/models/'; import type { ResolvedExtension } from '@console/dynamic-plugin-sdk'; @@ -11,18 +10,27 @@ import * as k8sModels from '.'; * Sample YAML manifests for some of the statically-defined Kubernetes models. */ -export const baseTemplates = ImmutableMap>() - .setIn( - ['DEFAULT', 'default'], +export const baseTemplates: Record> = (() => { + const m: Record> = {}; + const add = (gvk: string, name: string, yaml: string) => { + if (!m[gvk]) m[gvk] = {}; + m[gvk][name] = yaml; + }; + + add( + 'DEFAULT', + 'default', ` apiVersion: '' kind: '' metadata: name: example `, - ) - .setIn( - [referenceForModel(k8sModels.BuildConfigModel), 'default'], + ); + + add( + referenceForModel(k8sModels.BuildConfigModel), + 'default', ` apiVersion: build.openshift.io/v1 kind: BuildConfig @@ -47,9 +55,11 @@ spec: imageChange: {} - type: ConfigChange `, - ) - .setIn( - [referenceForModel(k8sModels.DeploymentModel), 'default'], + ); + + add( + referenceForModel(k8sModels.DeploymentModel), + 'default', ` apiVersion: apps/v1 kind: Deployment @@ -71,9 +81,11 @@ spec: ports: - containerPort: 8080 `, - ) - .setIn( - [referenceForModel(k8sModels.ConfigMapModel), 'default'], + ); + + add( + referenceForModel(k8sModels.ConfigMapModel), + 'default', ` apiVersion: v1 kind: ConfigMap @@ -88,9 +100,11 @@ data: property.2=value-2 property.3=value-3 `, - ) - .setIn( - [referenceForModel(k8sModels.CronJobModel), 'default'], + ); + + add( + referenceForModel(k8sModels.CronJobModel), + 'default', ` apiVersion: batch/v1 kind: CronJob @@ -111,9 +125,11 @@ spec: - date; echo Hello from the Kubernetes cluster restartPolicy: OnFailure `, - ) - .setIn( - [referenceForModel(k8sModels.CustomResourceDefinitionModel), 'default'], + ); + + add( + referenceForModel(k8sModels.CustomResourceDefinitionModel), + 'default', ` apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition @@ -156,9 +172,11 @@ spec: shortNames: - ct `, - ) - .setIn( - [referenceForModel(k8sModels.DeploymentConfigModel), 'default'], + ); + + add( + referenceForModel(k8sModels.DeploymentConfigModel), + 'default', ` apiVersion: apps.openshift.io/v1 kind: DeploymentConfig @@ -179,9 +197,11 @@ spec: ports: - containerPort: 8080 `, - ) - .setIn( - [referenceForModel(k8sModels.PersistentVolumeModel), 'default'], + ); + + add( + referenceForModel(k8sModels.PersistentVolumeModel), + 'default', ` apiVersion: v1 kind: PersistentVolume @@ -198,9 +218,11 @@ spec: path: /tmp server: 192.0.2.1 `, - ) - .setIn( - [referenceForModel(k8sModels.HorizontalPodAutoscalerModel), 'default'], + ); + + add( + referenceForModel(k8sModels.HorizontalPodAutoscalerModel), + 'default', ` apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler @@ -221,9 +243,11 @@ spec: averageUtilization: 80 type: Utilization `, - ) - .setIn( - [referenceForModel(k8sModels.PodModel), 'default'], + ); + + add( + referenceForModel(k8sModels.PodModel), + 'default', ` apiVersion: v1 kind: Pod @@ -247,9 +271,11 @@ spec: drop: - ALL `, - ) - .setIn( - [referenceForModel(k8sModels.JobModel), 'default'], + ); + + add( + referenceForModel(k8sModels.JobModel), + 'default', ` apiVersion: batch/v1 kind: Job @@ -267,18 +293,22 @@ spec: command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] restartPolicy: Never `, - ) - .setIn( - [referenceForModel(k8sModels.ImageStreamModel), 'default'], + ); + + add( + referenceForModel(k8sModels.ImageStreamModel), + 'default', ` apiVersion: image.openshift.io/v1 kind: ImageStream metadata: name: example `, - ) - .setIn( - [referenceForModel(k8sModels.RoleBindingModel), 'default'], + ); + + add( + referenceForModel(k8sModels.RoleBindingModel), + 'default', ` apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding @@ -293,9 +323,11 @@ roleRef: name: view apiGroup: rbac.authorization.k8s.io `, - ) - .setIn( - [referenceForModel(k8sModels.RoleModel), 'default'], + ); + + add( + referenceForModel(k8sModels.RoleModel), + 'default', `apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: @@ -305,9 +337,11 @@ rules: resources: ["pods"] verbs: ["get", "watch", "list"] `, - ) - .setIn( - [referenceForModel(k8sModels.RoleModel), 'read-pods-within-ns'], + ); + + add( + referenceForModel(k8sModels.RoleModel), + 'read-pods-within-ns', ` apiVersion: rbac.authorization.k8s.io/v1 kind: Role @@ -319,9 +353,11 @@ rules: resources: ["pods"] verbs: ["get", "list", "watch"] `, - ) - .setIn( - [referenceForModel(k8sModels.RoleModel), 'read-write-deployment-in-ext-and-apps-apis'], + ); + + add( + referenceForModel(k8sModels.RoleModel), + 'read-write-deployment-in-ext-and-apps-apis', ` apiVersion: rbac.authorization.k8s.io/v1 kind: Role @@ -333,9 +369,11 @@ rules: resources: ["deployments"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] `, - ) - .setIn( - [referenceForModel(k8sModels.RoleModel), 'read-pods-and-read-write-jobs'], + ); + + add( + referenceForModel(k8sModels.RoleModel), + 'read-pods-and-read-write-jobs', `apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: @@ -349,9 +387,11 @@ rules: resources: ["jobs"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] `, - ) - .setIn( - [referenceForModel(k8sModels.RoleModel), 'read-configmap-within-ns'], + ); + + add( + referenceForModel(k8sModels.RoleModel), + 'read-configmap-within-ns', ` apiVersion: rbac.authorization.k8s.io/v1 kind: Role @@ -364,9 +404,11 @@ rules: resourceNames: ["my-config"] verbs: ["get"] `, - ) - .setIn( - [referenceForModel(k8sModels.ClusterRoleModel), 'read-nodes'], + ); + + add( + referenceForModel(k8sModels.ClusterRoleModel), + 'read-nodes', ` apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -378,9 +420,11 @@ rules: resources: ["nodes"] verbs: ["get", "list", "watch"] `, - ) - .setIn( - [referenceForModel(k8sModels.ClusterRoleModel), 'get-and-post-to-non-resource-endpoints'], + ); + + add( + referenceForModel(k8sModels.ClusterRoleModel), + 'get-and-post-to-non-resource-endpoints', ` apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -391,9 +435,11 @@ rules: - nonResourceURLs: ["/healthz", "/healthz/*"] # '*' in a nonResourceURL is a suffix glob match verbs: ["get", "post"] `, - ) - .setIn( - [referenceForModel(k8sModels.DaemonSetModel), 'default'], + ); + + add( + referenceForModel(k8sModels.DaemonSetModel), + 'default', ` apiVersion: apps/v1 kind: DaemonSet @@ -414,9 +460,11 @@ spec: ports: - containerPort: 8080 `, - ) - .setIn( - [referenceForModel(k8sModels.PersistentVolumeClaimModel), 'default'], + ); + + add( + referenceForModel(k8sModels.PersistentVolumeClaimModel), + 'default', ` apiVersion: v1 kind: PersistentVolumeClaim @@ -430,9 +478,11 @@ spec: requests: storage: 1Gi `, - ) - .setIn( - [referenceForModel(k8sModels.ResourceQuotaModel), 'default'], + ); + + add( + referenceForModel(k8sModels.ResourceQuotaModel), + 'default', ` apiVersion: v1 kind: ResourceQuota @@ -446,9 +496,11 @@ spec: limits.cpu: "2" limits.memory: 2Gi `, - ) - .setIn( - [referenceForModel(k8sModels.LimitRangeModel), 'default'], + ); + + add( + referenceForModel(k8sModels.LimitRangeModel), + 'default', ` apiVersion: v1 kind: LimitRange @@ -462,9 +514,11 @@ spec: memory: 256Mi type: Container `, - ) - .setIn( - [referenceForModel(k8sModels.StatefulSetModel), 'default'], + ); + + add( + referenceForModel(k8sModels.StatefulSetModel), + 'default', ` apiVersion: apps/v1 kind: StatefulSet @@ -501,9 +555,11 @@ spec: requests: storage: 1Gi `, - ) - .setIn( - [referenceForModel(k8sModels.StorageClassModel), 'default'], + ); + + add( + referenceForModel(k8sModels.StorageClassModel), + 'default', ` apiVersion: storage.k8s.io/v1 kind: StorageClass @@ -512,9 +568,11 @@ metadata: provisioner: my-provisioner reclaimPolicy: Delete `, - ) - .setIn( - [referenceForModel(k8sModels.VolumeAttributesClassModel), 'default'], + ); + + add( + referenceForModel(k8sModels.VolumeAttributesClassModel), + 'default', ` apiVersion: storage.k8s.io/v1 kind: VolumeAttributesClass @@ -525,18 +583,22 @@ parameters: provisioned-iops: "3000" provisioned-throughput: "50" `, - ) - .setIn( - [referenceForModel(k8sModels.ServiceAccountModel), 'default'], + ); + + add( + referenceForModel(k8sModels.ServiceAccountModel), + 'default', ` apiVersion: v1 kind: ServiceAccount metadata: name: example `, - ) - .setIn( - [referenceForModel(k8sModels.SecretModel), 'default'], + ); + + add( + referenceForModel(k8sModels.SecretModel), + 'default', ` apiVersion: v1 kind: Secret @@ -547,9 +609,11 @@ stringData: username: admin password: opensesame `, - ) - .setIn( - [referenceForModel(k8sModels.ReplicaSetModel), 'default'], + ); + + add( + referenceForModel(k8sModels.ReplicaSetModel), + 'default', ` apiVersion: apps/v1 kind: ReplicaSet @@ -572,9 +636,11 @@ spec: ports: - containerPort: 8080 `, - ) - .setIn( - [referenceForModel(k8sModels.ReplicationControllerModel), 'default'], + ); + + add( + referenceForModel(k8sModels.ReplicationControllerModel), + 'default', ` apiVersion: v1 kind: ReplicationController @@ -596,9 +662,11 @@ spec: ports: - containerPort: 8080 `, - ) - .setIn( - [referenceForModel(k8sModels.BuildConfigModel), 'docker-build'], + ); + + add( + referenceForModel(k8sModels.BuildConfigModel), + 'docker-build', ` apiVersion: build.openshift.io/v1 kind: BuildConfig @@ -640,9 +708,11 @@ spec: - rake - test `, - ) - .setIn( - [referenceForModel(k8sModels.BuildConfigModel), 's2i-build'], + ); + + add( + referenceForModel(k8sModels.BuildConfigModel), + 's2i-build', `apiVersion: build.openshift.io/v1 kind: BuildConfig metadata: @@ -671,9 +741,11 @@ spec: imageChange: {} - type: ConfigChange `, - ) - .setIn( - [referenceForModel(k8sModels.GroupModel), 'default'], + ); + + add( + referenceForModel(k8sModels.GroupModel), + 'default', ` apiVersion: user.openshift.io/v1 kind: Group @@ -683,9 +755,11 @@ users: - user1 - user2 `, - ) - .setIn( - [referenceForModel(k8sModels.ResourceQuotaModel), 'rq-compute'], + ); + + add( + referenceForModel(k8sModels.ResourceQuotaModel), + 'rq-compute', ` apiVersion: v1 kind: ResourceQuota @@ -699,9 +773,11 @@ spec: limits.cpu: '2' limits.memory: 2Gi `, - ) - .setIn( - [referenceForModel(k8sModels.ResourceQuotaModel), 'rq-storageclass'], + ); + + add( + referenceForModel(k8sModels.ResourceQuotaModel), + 'rq-storageclass', ` apiVersion: v1 kind: ResourceQuota @@ -720,9 +796,11 @@ spec: bronze.storage-class.kubernetes.io/requests.storage: 1Gi bronze.storage-class.kubernetes.io/persistentvolumeclaims: '1' `, - ) - .setIn( - [referenceForModel(k8sModels.ResourceQuotaModel), 'rq-counts'], + ); + + add( + referenceForModel(k8sModels.ResourceQuotaModel), + 'rq-counts', ` apiVersion: v1 kind: ResourceQuota @@ -738,9 +816,11 @@ spec: services: "10" services.loadbalancers: "2" `, - ) - .setIn( - [referenceForModel(k8sModels.ClusterAutoscalerModel), 'default'], + ); + + add( + referenceForModel(k8sModels.ClusterAutoscalerModel), + 'default', ` apiVersion: "autoscaling.openshift.io/v1" kind: "ClusterAutoscaler" @@ -748,9 +828,11 @@ metadata: name: "default" spec: {} `, - ) - .setIn( - [referenceForModel(k8sModels.MachineDeploymentModel), 'default'], + ); + + add( + referenceForModel(k8sModels.MachineDeploymentModel), + 'default', ` apiVersion: "machine.openshift.io/v1beta1" kind: MachineDeployment @@ -770,9 +852,11 @@ spec: versions: kubelet: "" `, - ) - .setIn( - [referenceForModel(k8sModels.MachineSetModel), 'default'], + ); + + add( + referenceForModel(k8sModels.MachineSetModel), + 'default', ` apiVersion: "machine.openshift.io/v1beta1" kind: MachineSet @@ -791,9 +875,11 @@ spec: providerSpec: value: {} `, - ) - .setIn( - [referenceForModel(k8sModels.MachineModel), 'default'], + ); + + add( + referenceForModel(k8sModels.MachineModel), + 'default', ` apiVersion: "machine.openshift.io/v1beta1" kind: Machine @@ -802,9 +888,11 @@ metadata: spec: providerSpec: {} `, - ) - .setIn( - [referenceForModel(k8sModels.MachineConfigModel), 'default'], + ); + + add( + referenceForModel(k8sModels.MachineConfigModel), + 'default', ` apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig @@ -824,9 +912,11 @@ spec: contents: source: data:,example%20content `, - ) - .setIn( - [referenceForModel(k8sModels.MachineConfigPoolModel), 'default'], + ); + + add( + referenceForModel(k8sModels.MachineConfigPoolModel), + 'default', ` apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfigPool @@ -840,9 +930,11 @@ spec: matchLabels: node-role.kubernetes.io/master: "" `, - ) - .setIn( - [referenceForModel(k8sModels.MachineAutoscalerModel), 'default'], + ); + + add( + referenceForModel(k8sModels.MachineAutoscalerModel), + 'default', ` apiVersion: "autoscaling.openshift.io/v1beta1" kind: "MachineAutoscaler" @@ -857,9 +949,11 @@ spec: kind: MachineSet name: worker `, - ) - .setIn( - [referenceForModel(k8sModels.MachineHealthCheckModel), 'default'], + ); + + add( + referenceForModel(k8sModels.MachineHealthCheckModel), + 'default', ` apiVersion: machine.openshift.io/v1beta1 kind: MachineHealthCheck @@ -882,9 +976,11 @@ spec: timeout: "300s" maxUnhealthy: "40%" `, - ) - .setIn( - [referenceForModel(k8sModels.ConsolePluginModel), 'default'], + ); + + add( + referenceForModel(k8sModels.ConsolePluginModel), + 'default', ` apiVersion: console.openshift.io/v1 kind: ConsolePlugin @@ -899,9 +995,11 @@ spec: type: Service displayName: myConsolePlugin `, - ) - .setIn( - [referenceForModel(k8sModels.ConsoleLinkModel), 'default'], + ); + + add( + referenceForModel(k8sModels.ConsoleLinkModel), + 'default', ` apiVersion: console.openshift.io/v1 kind: ConsoleLink @@ -912,9 +1010,11 @@ spec: location: HelpMenu text: Help Menu Link `, - ) - .setIn( - [referenceForModel(k8sModels.ConsoleLinkModel), 'cl-user-menu'], + ); + + add( + referenceForModel(k8sModels.ConsoleLinkModel), + 'cl-user-menu', ` apiVersion: console.openshift.io/v1 kind: ConsoleLink @@ -925,9 +1025,11 @@ spec: location: UserMenu text: User Menu Link `, - ) - .setIn( - [referenceForModel(k8sModels.ConsoleLinkModel), 'cl-application-menu'], + ); + + add( + referenceForModel(k8sModels.ConsoleLinkModel), + 'cl-application-menu', ` apiVersion: console.openshift.io/v1 kind: ConsoleLink @@ -941,9 +1043,11 @@ spec: section: Example Section imageURL: data:image/svg+xml;base64,PHN2ZyBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyNCAyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTE4LjkgMi4xdjIuMWgtMS43di0yLjFjMC0uMSAwLS4yLS4xLS4zcy0uMi0uMS0uMy0uMWgtMTQuN2MtLjEgMC0uMiAwLS4zLjEgMCAuMS0uMS4yLS4xLjN2MTQuNmMwIC4xIDAgLjIuMS4zcy4yLjEuMy4xaDIuMXYxLjdoLTIuMWMtLjYgMC0xLjEtLjItMS41LS42LS40LS40LS42LS45LS42LTEuNXYtMTQuNmMwLS41LjItMS4xLjYtMS41czEtLjYgMS41LS42aDE0LjZjLjYgMCAxLjEuMiAxLjUuNnMuNyAxIC43IDEuNXptNS4xIDUuMnYxNC42YzAgLjYtLjIgMS4xLS42IDEuNXMtMSAuNi0xLjUuNmgtMTQuNmMtLjYgMC0xLjEtLjItMS41LS42cy0uNi0uOS0uNi0xLjV2LTE0LjZjMC0uNi4yLTEuMS42LTEuNXMuOS0uNiAxLjUtLjZoMTQuNmMuNiAwIDEuMS4yIDEuNS42cy42LjkuNiAxLjV6bS0xLjcgMTQuNnYtMTQuNmMwLS4xIDAtLjItLjEtLjNzLS4yLS4xLS4zLS4xaC0xNC42Yy0uMSAwLS4yIDAtLjMuMXMtLjEuMi0uMS4zdjE0LjZjMCAuMSAwIC4yLjEuM3MuMi4xLjMuMWgxNC42Yy4xIDAgLjIgMCAuMy0uMXMuMS0uMi4xLS4zeiIvPjwvc3ZnPg== `, - ) - .setIn( - [referenceForModel(k8sModels.ConsoleLinkModel), 'cl-namespace-dashboard'], + ); + + add( + referenceForModel(k8sModels.ConsoleLinkModel), + 'cl-namespace-dashboard', ` apiVersion: console.openshift.io/v1 kind: ConsoleLink @@ -957,9 +1061,11 @@ spec: namespaces: - default `, - ) - .setIn( - [referenceForModel(k8sModels.ConsoleLinkModel), 'cl-contact-mail'], + ); + + add( + referenceForModel(k8sModels.ConsoleLinkModel), + 'cl-contact-mail', ` apiVersion: console.openshift.io/v1 kind: ConsoleLink @@ -970,9 +1076,11 @@ spec: location: UserMenu text: Contact Mail Link `, - ) - .setIn( - [referenceForModel(k8sModels.ConsoleCLIDownloadModel), 'default'], + ); + + add( + referenceForModel(k8sModels.ConsoleCLIDownloadModel), + 'default', ` apiVersion: console.openshift.io/v1 kind: ConsoleCLIDownload @@ -988,9 +1096,11 @@ spec: - href: 'https://www.example.com' text: Download Example CLI `, - ) - .setIn( - [referenceForModel(k8sModels.ConsoleNotificationModel), 'default'], + ); + + add( + referenceForModel(k8sModels.ConsoleNotificationModel), + 'default', ` apiVersion: console.openshift.io/v1 kind: ConsoleNotification @@ -1005,9 +1115,11 @@ spec: color: '#fff' backgroundColor: '#0066cc' `, - ) - .setIn( - [referenceForModel(k8sModels.ConsoleExternalLogLinkModel), 'default'], + ); + + add( + referenceForModel(k8sModels.ConsoleExternalLogLinkModel), + 'default', ` apiVersion: console.openshift.io/v1 kind: ConsoleLogLink @@ -1017,9 +1129,11 @@ spec: hrefTemplate: 'https://example.com/logs?resourceName=\${resourceName}&containerName=\${containerName}&resourceNamespace=\${resourceNamespace}&podLabels=\${podLabels}' text: Example Logs `, - ) - .setIn( - [referenceForModel(appModels.QuickStartModel), 'default'], + ); + + add( + referenceForModel(appModels.QuickStartModel), + 'default', ` apiVersion: console.openshift.io/v1 kind: ConsoleQuickStart @@ -1050,7 +1164,7 @@ spec: instructions: |- #### Verify the image was successfully deployed: Do you see a **httpd-24-centos7** deployment? - failedTaskHelp: This task isn’t verified yet. Try the task again. + failedTaskHelp: This task isn't verified yet. Try the task again. summary: success: Great work! You deployed an example application using the **quay.io/centos7/httpd-24-centos7** image. failed: Try the steps again. @@ -1063,15 +1177,17 @@ spec: instructions: |- #### Verify your application is running: In the new tab, do you see the Apache HTTP server test page? - failedTaskHelp: This task isn’t verified yet. Try the task again. + failedTaskHelp: This task isn't verified yet. Try the task again. summary: success: Great work! You deployed the **quay.io/centos7/httpd-24-centos7** image. failed: Try the steps again. conclusion: Your example **httpd-24-centos7-app** application, using the **quay.io/centos7/httpd-24-centos7** image, is deployed and ready. `, - ) - .setIn( - [referenceForModel(k8sModels.ConsoleYAMLSampleModel), 'default'], + ); + + add( + referenceForModel(k8sModels.ConsoleYAMLSampleModel), + 'default', ` apiVersion: console.openshift.io/v1 kind: ConsoleYAMLSample @@ -1102,9 +1218,11 @@ spec: - "for i in 9 8 7 6 5 4 3 2 1 ; do echo $i ; done" restartPolicy: Never `, - ) - .setIn( - [referenceForModel(k8sModels.VolumeSnapshotModel), 'default'], + ); + + add( + referenceForModel(k8sModels.VolumeSnapshotModel), + 'default', ` apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot @@ -1115,9 +1233,11 @@ spec: source: persistentVolumeClaimName: pvc-test `, - ) - .setIn( - [referenceForModel(k8sModels.VolumeSnapshotClassModel), 'default'], + ); + + add( + referenceForModel(k8sModels.VolumeSnapshotClassModel), + 'default', ` apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass @@ -1126,9 +1246,11 @@ metadata: driver: hostpath.csi.k8s.io #csi-hostpath deletionPolicy: Delete `, - ) - .setIn( - [referenceForModel(k8sModels.VolumeSnapshotContentModel), 'default'], + ); + + add( + referenceForModel(k8sModels.VolumeSnapshotContentModel), + 'default', ` apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotContent @@ -1144,9 +1266,11 @@ spec: name: example-snap namespace: default `, - ) - .setIn( - [referenceForModel(PodDisruptionBudgetModel), 'default'], + ); + + add( + referenceForModel(PodDisruptionBudgetModel), + 'default', ` apiVersion: policy/v1 kind: PodDisruptionBudget @@ -1157,9 +1281,11 @@ spec: matchLabels: app: hello-openshift `, - ) - .setIn( - [referenceForModel(PodDisruptionBudgetModel), 'pdb-max-unavailable'], + ); + + add( + referenceForModel(PodDisruptionBudgetModel), + 'pdb-max-unavailable', ` apiVersion: policy/v1 kind: PodDisruptionBudget @@ -1172,9 +1298,11 @@ spec: matchLabels: app: hello-openshift `, - ) - .setIn( - [referenceForModel(PodDisruptionBudgetModel), 'pdb-min-available'], + ); + + add( + referenceForModel(PodDisruptionBudgetModel), + 'pdb-min-available', ` apiVersion: policy/v1 kind: PodDisruptionBudget @@ -1187,9 +1315,11 @@ spec: matchLabels: app: hello-openshift `, - ) - .setIn( - [referenceForModel(k8sModels.ServiceMonitorModel), 'default'], + ); + + add( + referenceForModel(k8sModels.ServiceMonitorModel), + 'default', ` apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor @@ -1206,16 +1336,22 @@ spec: `, ); -export const getYAMLTemplates = (extensionTemplates: ResolvedExtension[] = []) => - ImmutableMap>() - .merge(baseTemplates) - .withMutations((map) => { - extensionTemplates.forEach((yt) => { - const modelRef = referenceForExtensionModel(yt.properties.model); - const templateName = yt.properties.name || 'default'; + return m; +})(); - if (!baseTemplates.hasIn([modelRef, templateName])) { - map.setIn([modelRef, templateName], yt.properties.template); - } - }); - }); +export const getYAMLTemplates = ( + extensionTemplates: ResolvedExtension[] = [], +): Record> => { + const result: Record> = Object.fromEntries( + Object.entries(baseTemplates).map(([k, v]) => [k, { ...v }]), + ); + extensionTemplates.forEach((yt) => { + const modelRef = referenceForExtensionModel(yt.properties.model); + const templateName = yt.properties.name || 'default'; + if (!baseTemplates[modelRef]?.[templateName]) { + if (!result[modelRef]) result[modelRef] = {}; + result[modelRef][templateName] = yt.properties.template; + } + }); + return result; +}; diff --git a/frontend/public/module/k8s/__tests__/k8s-models.spec.ts b/frontend/public/module/k8s/__tests__/k8s-models.spec.ts index 0c91b4628c7..22d36529476 100644 --- a/frontend/public/module/k8s/__tests__/k8s-models.spec.ts +++ b/frontend/public/module/k8s/__tests__/k8s-models.spec.ts @@ -89,14 +89,14 @@ describe('versionForReference', () => { describe('modelsToMap', () => { it('returns a map with keys based on model.kind for models with crd:false', () => { - expect(modelsToMap([PodModel, DeploymentModel]).toObject()).toEqual({ + expect(modelsToMap([PodModel, DeploymentModel])).toEqual({ [PodModel.kind]: PodModel, [DeploymentModel.kind]: DeploymentModel, }); }); it('returns a map with keys based on referenceForModel for models with crd:true', () => { - expect(modelsToMap([ClusterResourceQuotaModel, PrometheusModel]).toObject()).toEqual({ + expect(modelsToMap([ClusterResourceQuotaModel, PrometheusModel])).toEqual({ [referenceForModel(ClusterResourceQuotaModel)]: ClusterResourceQuotaModel, [referenceForModel(PrometheusModel)]: PrometheusModel, }); diff --git a/frontend/public/module/k8s/k8s-models.ts b/frontend/public/module/k8s/k8s-models.ts index fbe21b251b6..793ff181b85 100644 --- a/frontend/public/module/k8s/k8s-models.ts +++ b/frontend/public/module/k8s/k8s-models.ts @@ -1,4 +1,3 @@ -import { Map as ImmutableMap } from 'immutable'; import * as _ from 'lodash'; import type { K8sResourceKindReference, ModelMetadata } from '@console/dynamic-plugin-sdk'; import { isModelMetadata } from '@console/dynamic-plugin-sdk'; @@ -20,16 +19,19 @@ import { referenceForModel, referenceForGroupVersionKind } from './k8s-ref'; const modelKey = (model: K8sKind): string => // TODO: Use `referenceForModel` even for known API objects model.crd ? referenceForModel(model) : model.kind; -export const modelsToMap = (models: K8sKind[]): ImmutableMap => - ImmutableMap().withMutations((map) => { - models.forEach((model) => map.set(modelKey(model), model)); +export const modelsToMap = (models: K8sKind[]): Record => { + const map: Record = {}; + models.forEach((model) => { + map[modelKey(model)] = model; }); + return map; +}; /** * Contains static resource definitions for Kubernetes objects. * Keys are of type `group:version:Kind`, but TypeScript doesn't support regex types (https://github.com/Microsoft/TypeScript/issues/6579). */ -let k8sModels; +let k8sModels: Record; const getK8sModels = () => { if (!k8sModels) { @@ -47,25 +49,35 @@ export const modelFor = (ref: K8sResourceKindReference): K8sModel => { .getExtensions() .filter(isModelMetadata) as LoadedExtension[]; - let m = getK8sModels().get(ref); + let m = getK8sModels()[ref]; if (m) { - const metadata = getModelExtensionMetadata(metadataExtensions, m?.group, m?.version, m?.kind); - return _.merge(m, metadata); + const metadata = getModelExtensionMetadata( + metadataExtensions, + m?.apiGroup, + m?.apiVersion, + m?.kind, + ); + return _.merge({}, m, metadata); } // FIXME: Remove synchronous `store.getState()` call here, should be using `connectToModels` instead, only here for backwards-compatibility - m = store.getState().k8s.getIn(['RESOURCES', 'models']).get(ref); + m = store.getState().k8s.RESOURCES?.models?.[ref]; if (m) { return m; } - m = getK8sModels().get(kindForReference(ref)); + m = getK8sModels()[kindForReference(ref)]; if (m) { - const metadata = getModelExtensionMetadata(metadataExtensions, m?.group, m?.version, m?.kind); - return _.merge(m, metadata); + const metadata = getModelExtensionMetadata( + metadataExtensions, + m?.apiGroup, + m?.apiVersion, + m?.kind, + ); + return _.merge({}, m, metadata); } - m = store.getState().k8s.getIn(['RESOURCES', 'models']).get(kindForReference(ref)); + m = store.getState().k8s.RESOURCES?.models?.[kindForReference(ref)]; if (m) { return m; } @@ -76,26 +88,23 @@ export const modelFor = (ref: K8sResourceKindReference): K8sModel => { * NOTE: This will not work for CRDs defined at runtime, use `connectToModels` instead. */ export const modelForGroupKind = (group: string, kind: string): K8sKind => { - const models: ImmutableMap = store.getState().k8s.getIn(['RESOURCES', 'models']); - const groupVersionMap: DiscoveryResources['groupVersionMap'] = store - .getState() - .k8s.getIn(['RESOURCES', 'groupToVersionMap']); + const models: Record = store.getState().k8s.RESOURCES?.models ?? {}; + const groupVersionMap: DiscoveryResources['groupVersionMap'] = + store.getState().k8s.RESOURCES?.groupToVersionMap; const { preferredVersion, versions } = groupVersionMap?.[group] || {}; if (preferredVersion) { - // Find a model for the CRD that uses this preferred version const ref = referenceForGroupVersionKind(group)(preferredVersion)(kind); - const model = models.get(ref); + const model = models[ref]; if (model) { return model; } } - // In case the preferred version does not have the CRD if (versions) { - const sortedVersions: string[] = versions.sort(apiVersionCompare); + const sortedVersions: string[] = [...versions].sort(apiVersionCompare); for (const version of sortedVersions) { const ref = referenceForGroupVersionKind(group)(version)(kind); - const model = models.get(ref); + const model = models[ref]; if (model) { return model; } @@ -117,16 +126,14 @@ export const useModelFinder = () => { const referenceForGroupVersionPlural = (group: string) => (version: string) => (plural: string) => [group || 'core', version, plural].join('~'); - const models = useConsoleSelector>(({ k8s }) => - k8s.getIn(['RESOURCES', 'models']), - ); - const pluralsToModelMap = models.reduce((acc, curr) => { + const models = useConsoleSelector>(({ k8s }) => k8s.RESOURCES?.models); + const pluralsToModelMap = Object.values(models ?? {}).reduce((acc, curr) => { const ref = referenceForGroupVersionPlural(curr.apiGroup)(curr.apiVersion)(curr.plural); acc[ref] = curr; return acc; }, {}); - const groupVersionMap = useConsoleSelector(({ k8s }) => - k8s.getIn(['RESOURCES', 'groupToVersionMap']), + const groupVersionMap = useConsoleSelector( + ({ k8s }) => k8s.RESOURCES?.groupToVersionMap, ); const findModel = (group: string, resource: string) => { @@ -139,16 +146,14 @@ export const useModelFinder = () => { } const { preferredVersion, versions } = groupVersionMap?.[group] || {}; if (preferredVersion) { - // Find a model for the CRD that uses this preferred version const ref = referenceForGroupVersionPlural(group)(preferredVersion)(resource); const model = pluralsToModelMap[ref]; if (model) { return model; } } - // In case the preferred version does not have the CRD if (versions) { - const sortedVersions: string[] = versions.sort(apiVersionCompare); + const sortedVersions: string[] = [...versions].sort(apiVersionCompare); for (const version of sortedVersions) { const ref = referenceForGroupVersionPlural(group)(version)(resource); const model = pluralsToModelMap[ref]; diff --git a/frontend/public/plugins.ts b/frontend/public/plugins.ts index ee1c8a2aa85..3315f54cfcd 100644 --- a/frontend/public/plugins.ts +++ b/frontend/public/plugins.ts @@ -119,7 +119,7 @@ export const featureFlagMiddleware: Middleware<{}, RootState> = (s) => { if (nextFlags !== prevFlags) { prevFlags = nextFlags; - pluginStore.setFeatureFlags(nextFlags.toObject()); + pluginStore.setFeatureFlags({ ...nextFlags }); } return result; diff --git a/frontend/public/reducers/__tests__/dashboards.spec.ts b/frontend/public/reducers/__tests__/dashboards.spec.ts index 4f170f8f50a..15f4cff42d1 100644 --- a/frontend/public/reducers/__tests__/dashboards.spec.ts +++ b/frontend/public/reducers/__tests__/dashboards.spec.ts @@ -1,4 +1,3 @@ -import * as Immutable from 'immutable'; import { noop } from 'lodash'; import { activateWatch, @@ -14,89 +13,121 @@ describe('dashboardsReducer', () => { it('returns default values if state is uninitialized', () => { const newState = dashboardsReducer(null, null); - expect(newState).toEqual(Immutable.Map(defaults)); + expect(newState).toEqual({ ...defaults }); }); it('activates new watch', () => { const action = activateWatch(RESULTS_TYPE.URL, 'fooUrl'); - const initialState = Immutable.Map(defaults); + const initialState = { ...defaults }; const newState = dashboardsReducer(initialState, action); - expect(newState).toEqual(initialState.setIn([RESULTS_TYPE.URL, 'fooUrl', 'active'], 1)); + expect(newState).toEqual({ + ...initialState, + [RESULTS_TYPE.URL]: { fooUrl: { active: 1 } }, + }); }); it('increments watch active prop', () => { const action = activateWatch(RESULTS_TYPE.URL, 'fooUrl'); - const initialState = Immutable.Map(defaults).setIn([RESULTS_TYPE.URL, 'fooUrl', 'active'], 1); + const initialState = { + ...defaults, + [RESULTS_TYPE.URL]: { fooUrl: { active: 1 } }, + }; const newState = dashboardsReducer(initialState, action); - expect(newState).toEqual(initialState.setIn([RESULTS_TYPE.URL, 'fooUrl', 'active'], 2)); + expect(newState).toEqual({ + ...initialState, + [RESULTS_TYPE.URL]: { fooUrl: { active: 2 } }, + }); }); it('updates watch timeout reference', () => { - const timeout = { ref: noop, refresh: noop, unref: noop } as NodeJS.Timer; + const timeout = { ref: noop, refresh: noop, unref: noop } as unknown as ReturnType< + typeof setTimeout + >; const action = updateWatchTimeout(RESULTS_TYPE.URL, 'fooUrl', timeout); - const initialState = Immutable.Map(defaults); + const initialState = { ...defaults }; const stateWithTimeout = dashboardsReducer(initialState, action); - expect(stateWithTimeout).toEqual( - initialState.setIn([RESULTS_TYPE.URL, 'fooUrl', 'timeout'], timeout), - ); + expect(stateWithTimeout).toEqual({ + ...initialState, + [RESULTS_TYPE.URL]: { fooUrl: { timeout } }, + }); - const nextTimeout = { ref: noop, refresh: noop, unref: noop } as NodeJS.Timer; + const nextTimeout = { ref: noop, refresh: noop, unref: noop } as unknown as ReturnType< + typeof setTimeout + >; const nextAction = updateWatchTimeout(RESULTS_TYPE.URL, 'fooUrl', nextTimeout); const nextState = dashboardsReducer(stateWithTimeout, nextAction); - expect(nextState).toEqual( - stateWithTimeout.setIn([RESULTS_TYPE.URL, 'fooUrl', 'timeout'], nextTimeout), - ); + expect(nextState).toEqual({ + ...stateWithTimeout, + [RESULTS_TYPE.URL]: { + fooUrl: { ...stateWithTimeout[RESULTS_TYPE.URL].fooUrl, timeout: nextTimeout }, + }, + }); }); it('updates in flight resource', () => { const action = updateWatchInFlight(RESULTS_TYPE.URL, 'fooUrl', true); - const initialState = Immutable.Map(defaults); + const initialState = { ...defaults }; const stateInFlight = dashboardsReducer(initialState, action); - expect(stateInFlight).toEqual( - initialState.setIn([RESULTS_TYPE.URL, 'fooUrl', 'inFlight'], true), - ); + expect(stateInFlight).toEqual({ + ...initialState, + [RESULTS_TYPE.URL]: { fooUrl: { inFlight: true } }, + }); const nextAction = updateWatchInFlight(RESULTS_TYPE.URL, 'fooUrl', false); const nextState = dashboardsReducer(stateInFlight, nextAction); - expect(nextState).toEqual(stateInFlight.setIn([RESULTS_TYPE.URL, 'fooUrl', 'inFlight'], false)); + expect(nextState).toEqual({ + ...stateInFlight, + [RESULTS_TYPE.URL]: { + fooUrl: { ...stateInFlight[RESULTS_TYPE.URL].fooUrl, inFlight: false }, + }, + }); }); it('stops watch', () => { - const timeout = { ref: noop, refresh: noop, unref: noop } as NodeJS.Timer; + const timeout = { ref: noop, refresh: noop, unref: noop } as unknown as ReturnType< + typeof setTimeout + >; const action = stopWatch(RESULTS_TYPE.URL, 'fooUrl'); - const initialState = Immutable.Map(defaults).merge({ + const initialState = { + ...defaults, [RESULTS_TYPE.URL]: { fooUrl: { active: 2, timeout } }, - }); + }; const newState = dashboardsReducer(initialState, action); - expect(newState).toEqual(initialState.setIn([RESULTS_TYPE.URL, 'fooUrl', 'active'], 1)); + expect(newState).toEqual({ + ...initialState, + [RESULTS_TYPE.URL]: { fooUrl: { active: 1, timeout } }, + }); const nextState = dashboardsReducer(newState, action); - expect(nextState).toEqual(newState.setIn([RESULTS_TYPE.URL, 'fooUrl', 'active'], 0)); + expect(nextState).toEqual({ + ...newState, + [RESULTS_TYPE.URL]: { fooUrl: { active: 0, timeout } }, + }); }); it('updates result', () => { const action = setData(RESULTS_TYPE.URL, 'fooUrl', 'result'); - const initialState = Immutable.Map(defaults); + const initialState = { ...defaults }; const newState = dashboardsReducer(initialState, action); - expect(newState).toEqual( - initialState.withMutations((s) => - s - .setIn([RESULTS_TYPE.URL, 'fooUrl', 'data'], 'result') - .setIn([RESULTS_TYPE.URL, 'fooUrl', 'loadError'], null), - ), - ); + expect(newState).toEqual({ + ...initialState, + [RESULTS_TYPE.URL]: { fooUrl: { data: 'result', loadError: null } }, + }); const nextAction = setData(RESULTS_TYPE.URL, 'fooUrl', 'newResult'); const nextState = dashboardsReducer(newState, nextAction); - expect(nextState).toEqual(newState.setIn([RESULTS_TYPE.URL, 'fooUrl', 'data'], 'newResult')); + expect(nextState).toEqual({ + ...newState, + [RESULTS_TYPE.URL]: { fooUrl: { data: 'newResult', loadError: null } }, + }); }); }); diff --git a/frontend/public/reducers/__tests__/features.spec.tsx b/frontend/public/reducers/__tests__/features.spec.tsx index 2ec464a434d..f624cf4bfc5 100644 --- a/frontend/public/reducers/__tests__/features.spec.tsx +++ b/frontend/public/reducers/__tests__/features.spec.tsx @@ -1,5 +1,4 @@ import type { FC } from 'react'; -import * as Immutable from 'immutable'; import * as _ from 'lodash'; import { setFlag } from '@console/internal/actions/flags'; import { FLAGS } from '@console/shared/src/constants/common'; @@ -13,58 +12,59 @@ describe('featureReducer', () => { it('returns default values if state is uninitialized', () => { const newState = featureReducer(null, null); - expect(newState).toEqual( - Immutable.Map({ - AUTH_ENABLED: true, - PROMETHEUS: undefined, - OPENSHIFT: undefined, - MONITORING: false, - CAN_CREATE_NS: undefined, - CAN_GET_NS: undefined, - CAN_LIST_NS: undefined, - CAN_LIST_NODE: undefined, - CAN_LIST_PV: undefined, - CAN_LIST_CRD: undefined, - CAN_LIST_USERS: undefined, - CAN_LIST_GROUPS: undefined, - CAN_LIST_OPERATOR_GROUP: undefined, - CAN_LIST_PACKAGE_MANIFEST: undefined, - CAN_CREATE_PROJECT: undefined, - CAN_LIST_VSC: undefined, - CLUSTER_AUTOSCALER: undefined, - SHOW_OPENSHIFT_START_GUIDE: undefined, - CLUSTER_API: undefined, - CLUSTER_VERSION: undefined, - MACHINE_CONFIG: undefined, - MACHINE_AUTOSCALER: undefined, - MACHINE_HEALTH_CHECK: undefined, - CONSOLE_LINK: undefined, - CONSOLE_CLI_DOWNLOAD: undefined, - CONSOLE_NOTIFICATION: undefined, - CONSOLE_EXTERNAL_LOG_LINK: undefined, - CONSOLE_YAML_SAMPLE: undefined, - CONSOLE_QUICKSTART: undefined, - CONSOLE_CAPABILITY_LIGHTSPEEDBUTTON_IS_ENABLED: undefined, - CONSOLE_CAPABILITY_GETTINGSTARTEDBANNER_IS_ENABLED: undefined, - CONSOLE_CAPABILITY_GUIDEDTOUR_IS_ENABLED: undefined, - LIGHTSPEED_IS_AVAILABLE_TO_INSTALL: undefined, - DEVCONSOLE_PROXY: true, - VAC_PLATFORM_SUPPORT: undefined, - }), - ); + expect(newState).toStrictEqual({ + AUTH_ENABLED: true, + PROMETHEUS: undefined, + OPENSHIFT: undefined, + MONITORING: false, + CAN_CREATE_NS: undefined, + CAN_GET_NS: undefined, + CAN_LIST_NS: undefined, + CAN_LIST_NODE: undefined, + CAN_LIST_PV: undefined, + CAN_LIST_CRD: undefined, + CAN_LIST_USERS: undefined, + CAN_LIST_GROUPS: undefined, + CAN_LIST_OPERATOR_GROUP: undefined, + CAN_LIST_PACKAGE_MANIFEST: undefined, + CAN_CREATE_PROJECT: undefined, + CAN_LIST_VSC: undefined, + CLUSTER_AUTOSCALER: undefined, + SHOW_OPENSHIFT_START_GUIDE: undefined, + CLUSTER_API: undefined, + CLUSTER_VERSION: undefined, + MACHINE_CONFIG: undefined, + MACHINE_AUTOSCALER: undefined, + MACHINE_HEALTH_CHECK: undefined, + CONSOLE_LINK: undefined, + CONSOLE_CLI_DOWNLOAD: undefined, + CONSOLE_NOTIFICATION: undefined, + CONSOLE_EXTERNAL_LOG_LINK: undefined, + CONSOLE_YAML_SAMPLE: undefined, + CONSOLE_QUICKSTART: undefined, + CONSOLE_CAPABILITY_LIGHTSPEEDBUTTON_IS_ENABLED: undefined, + CONSOLE_CAPABILITY_GETTINGSTARTEDBANNER_IS_ENABLED: undefined, + CONSOLE_CAPABILITY_GUIDEDTOUR_IS_ENABLED: undefined, + LIGHTSPEED_IS_AVAILABLE_TO_INSTALL: undefined, + DEVCONSOLE_PROXY: true, + VAC_PLATFORM_SUPPORT: undefined, + }); }); it('returns updated state with new flags if `setFlag` action', () => { const action = setFlag(FLAGS.OPENSHIFT, true); - const initialState = Immutable.Map(defaults); + const initialState = { ...defaults }; const newState = featureReducer(initialState, action); - expect(newState).toEqual(initialState.merge({ [action.payload.flag]: action.payload.value })); + expect(newState).toStrictEqual({ + ...initialState, + [action.payload.flag]: action.payload.value, + }); }); it('returns state if not `setFlag` action', () => { const action = { type: 'OTHER_ACTION' } as any; - const initialState = Immutable.Map(defaults); + const initialState = { ...defaults }; const newState = featureReducer(initialState, action); expect(newState).toEqual(initialState); @@ -81,24 +81,23 @@ describe('featureReducer', () => { safeResources: [], groupVersionMap: {}, }); - const initialState = Immutable.Map(defaults); + const initialState = { ...defaults }; const newState = featureReducer(initialState, action); - expect(newState).toEqual( - initialState.merge({ - [FLAGS.PROMETHEUS]: false, - [FLAGS.CLUSTER_API]: false, - [FLAGS.MACHINE_CONFIG]: false, - [FLAGS.MACHINE_AUTOSCALER]: false, - [FLAGS.MACHINE_HEALTH_CHECK]: false, - [FLAGS.CONSOLE_LINK]: false, - [FLAGS.CONSOLE_CLI_DOWNLOAD]: false, - [FLAGS.CONSOLE_NOTIFICATION]: false, - [FLAGS.CONSOLE_EXTERNAL_LOG_LINK]: false, - [FLAGS.CONSOLE_YAML_SAMPLE]: false, - [FLAGS.CLUSTER_AUTOSCALER]: false, - }), - ); + expect(newState).toStrictEqual({ + ...initialState, + [FLAGS.PROMETHEUS]: false, + [FLAGS.CLUSTER_API]: false, + [FLAGS.MACHINE_CONFIG]: false, + [FLAGS.MACHINE_AUTOSCALER]: false, + [FLAGS.MACHINE_HEALTH_CHECK]: false, + [FLAGS.CONSOLE_LINK]: false, + [FLAGS.CONSOLE_CLI_DOWNLOAD]: false, + [FLAGS.CONSOLE_NOTIFICATION]: false, + [FLAGS.CONSOLE_EXTERNAL_LOG_LINK]: false, + [FLAGS.CONSOLE_YAML_SAMPLE]: false, + [FLAGS.CLUSTER_AUTOSCALER]: false, + }); }); }); @@ -117,11 +116,11 @@ describe('connectToFlags', () => { describe('stateToFlagsObject', () => { it('maps the desired flags to a new object', () => { - const featureState: FeatureState = Immutable.Map({ + const featureState: FeatureState = { FOO: true, BAR: false, QUX: undefined, - }); + }; expect( _.isEqual(stateToFlagsObject(featureState, ['BAR', 'QUX']), { @@ -142,11 +141,11 @@ describe('stateToFlagsObject', () => { describe('getFlagsObject', () => { it('maps the root state to feature sub-state as a new object', () => { - const featureState: FeatureState = Immutable.Map({ + const featureState: FeatureState = { FOO: true, BAR: false, QUX: undefined, - }); + }; const rootState = { [featureReducerName]: featureState, diff --git a/frontend/public/reducers/connectToFlags.ts b/frontend/public/reducers/connectToFlags.ts index 65d540a7fac..a9a89338c64 100644 --- a/frontend/public/reducers/connectToFlags.ts +++ b/frontend/public/reducers/connectToFlags.ts @@ -5,7 +5,7 @@ import type { RootState } from '../redux'; import type { FeatureState, FlagsObject } from './features'; export const stateToFlagsObject = (state: FeatureState, desiredFlags: string[]): FlagsObject => - desiredFlags.reduce((allFlags, f) => ({ ...allFlags, [f]: state.get(f) }), {} as FlagsObject); + desiredFlags.reduce((allFlags, f) => ({ ...allFlags, [f]: state[f] }), {} as FlagsObject); const stateToProps = (state: FeatureState, desiredFlags: string[]): WithFlagsProps => ({ flags: stateToFlagsObject(state, desiredFlags), diff --git a/frontend/public/reducers/dashboard-results.ts b/frontend/public/reducers/dashboard-results.ts index 1e187c7fc20..a5f86eb2d00 100644 --- a/frontend/public/reducers/dashboard-results.ts +++ b/frontend/public/reducers/dashboard-results.ts @@ -7,4 +7,4 @@ export enum RESULTS_TYPE { } export const isWatchActive = (state: DashboardsState, type: string, key: string): boolean => - state.getIn([type, key, 'active']) > 0 || state.getIn([type, key, 'inFlight']); + state[type]?.[key]?.active > 0 || state[type]?.[key]?.inFlight; diff --git a/frontend/public/reducers/dashboards.ts b/frontend/public/reducers/dashboards.ts index 0b693aab6d2..d498c7d9062 100644 --- a/frontend/public/reducers/dashboards.ts +++ b/frontend/public/reducers/dashboards.ts @@ -1,58 +1,89 @@ -import { fromJS, Map as ImmutableMap } from 'immutable'; import type { RequestMap } from '@console/dynamic-plugin-sdk/src/api/internal-types'; import type { DashboardsAction } from '../actions/dashboards'; import { ActionType } from '../actions/dashboards'; import { RESULTS_TYPE } from './dashboard-results'; export const defaults = { - [RESULTS_TYPE.PROMETHEUS]: fromJS({}), - [RESULTS_TYPE.URL]: fromJS({}), + [RESULTS_TYPE.PROMETHEUS]: {}, + [RESULTS_TYPE.URL]: {}, }; -export type DashboardsState = ImmutableMap>; +export type DashboardsState = Record>; + +const setIn = ( + state: DashboardsState, + type: string, + key: string, + prop: string, + value: any, +): DashboardsState => ({ + ...state, + [type]: { + ...state[type], + [key]: { + ...state[type]?.[key], + [prop]: value, + }, + }, +}); export const dashboardsReducer = ( state: DashboardsState, action: DashboardsAction, ): DashboardsState => { if (!state) { - return ImmutableMap(defaults); + return { ...defaults }; } switch (action.type) { case ActionType.ActivateWatch: { - const activePath = [action.payload.type, action.payload.key, 'active']; - const active = state.hasIn(activePath) ? state.getIn(activePath) : 0; - return state.setIn(activePath, active + 1); + const active = state[action.payload.type]?.[action.payload.key]?.active ?? 0; + return setIn(state, action.payload.type, action.payload.key, 'active', active + 1); } case ActionType.UpdateWatchTimeout: - return state.setIn( - [action.payload.type, action.payload.key, 'timeout'], + return setIn( + state, + action.payload.type, + action.payload.key, + 'timeout', action.payload.timeout, ); case ActionType.UpdateWatchInFlight: - return state.setIn( - [action.payload.type, action.payload.key, 'inFlight'], + return setIn( + state, + action.payload.type, + action.payload.key, + 'inFlight', action.payload.inFlight, ); case ActionType.StopWatch: { - const active = state.getIn([action.payload.type, action.payload.key, 'active']); - const newState = state.setIn([action.payload.type, action.payload.key, 'active'], active - 1); + const active = state[action.payload.type]?.[action.payload.key]?.active; if (active === 1) { - clearTimeout(state.getIn([action.payload.type, action.payload.key, 'timeout'])); + clearTimeout(state[action.payload.type]?.[action.payload.key]?.timeout); } - return newState; + return setIn(state, action.payload.type, action.payload.key, 'active', active - 1); } case ActionType.SetError: - return state.setIn( - [action.payload.type, action.payload.key, 'loadError'], + return setIn( + state, + action.payload.type, + action.payload.key, + 'loadError', action.payload.error, ); - case ActionType.SetData: - return state.withMutations((s) => - s - .setIn([action.payload.type, action.payload.key, 'data'], action.payload.data) - .setIn([action.payload.type, action.payload.key, 'loadError'], null), - ); + case ActionType.SetData: { + const { type, key, data } = action.payload; + return { + ...state, + [type]: { + ...state[type], + [key]: { + ...state[type]?.[key], + data, + loadError: null, + }, + }, + }; + } default: return state; } diff --git a/frontend/public/reducers/features.ts b/frontend/public/reducers/features.ts index 36566133b49..3f4ca977b7f 100644 --- a/frontend/public/reducers/features.ts +++ b/frontend/public/reducers/features.ts @@ -1,5 +1,3 @@ -/* eslint-disable no-barrel-files/no-barrel-files */ -import { Map as ImmutableMap } from 'immutable'; import * as _ from 'lodash'; import type { FeatureState } from '@console/dynamic-plugin-sdk/src/app/features'; import { ActionType as K8sActionType } from '@console/dynamic-plugin-sdk/src/app/k8s/actions/k8s'; @@ -26,8 +24,7 @@ import { referenceForGroupVersionKind, referenceForModel } from '../module/k8s/k import { pluginStore } from '../plugins'; import type { RootState } from '../redux'; -// eslint-disable-next-line prettier/prettier -export type { FeatureState }; +export type { FeatureState }; // eslint-disable-line no-barrel-files/no-barrel-files -- TODO, rewrite imports export const defaults = _.mapValues(FLAGS, (flag) => { switch (flag) { @@ -81,19 +78,21 @@ const getModelRef = (e: ModelFeatureFlag) => { export const featureReducerName = 'FLAGS'; export const featureReducer = (state: FeatureState, action: FeatureAction): FeatureState => { if (!state) { - return ImmutableMap(defaults); + return { ...defaults }; } switch (action.type) { case ActionType.SetFlag: - return state.set(action.payload.flag, action.payload.value); + if (state[action.payload.flag] === action.payload.value) return state; + return { ...state, [action.payload.flag]: action.payload.value }; - case ActionType.ClearSSARFlags: - return state.withMutations((s) => - action.payload.flags.reduce((acc, curr) => acc.remove(curr), s), - ); + case ActionType.ClearSSARFlags: { + const result = { ...state }; + action.payload.flags.forEach((flag) => delete result[flag]); + return result; + } - case ActionType.UpdateModelFlags: + case ActionType.UpdateModelFlags: { action.payload.added.forEach((e) => { addToCRDs(getModelRef(e), e.properties.flag); }); @@ -102,46 +101,52 @@ export const featureReducer = (state: FeatureState, action: FeatureAction): Feat delete CRDs[getModelRef(e)]; }); - return state.withMutations((s) => { - const allReferences: Set = action.payload.models.reduce( - (acc: Set, curr: K8sModel) => acc.add(referenceForModel(curr)), - new Set(), - ); - - // Evaluate new model flags - // TODO: Handle model flag removals (when plugin removal without a refresh is supported in console) - return action.payload.added.reduce((nextState, e) => { - const detected = allReferences.has(getModelRef(e)); - if (detected) { - // eslint-disable-next-line no-console - console.log(`${e.properties.flag} was detected.`); - } - return nextState.set(e.properties.flag, detected); - }, s); + const allReferences: Set = action.payload.models.reduce( + (acc: Set, curr: K8sModel) => acc.add(referenceForModel(curr)), + new Set(), + ); + + const updates: Record = {}; + // Evaluate new model flags + // TODO: Handle model flag removals (when plugin removal without a refresh is supported in console) + action.payload.added.forEach((e) => { + const detected = allReferences.has(getModelRef(e)); + if (detected) { + // eslint-disable-next-line no-console + console.log(`${e.properties.flag} was detected.`); + } + updates[e.properties.flag] = detected; }); - case K8sActionType.ReceivedResources: - // Flip all flags to false to signify that we did not see them - // eslint-disable-next-line no-param-reassign - _.each(CRDs, (v) => (state = state.set(v, false))); + return { ...state, ...updates }; + } + + case K8sActionType.ReceivedResources: { + const flagUpdates: Record = {}; + _.each(CRDs, (v) => { + flagUpdates[v] = false; + }); - return action.payload.resources.models + action.payload.resources.models .filter((model) => CRDs[referenceForModel(model)] !== undefined) - .reduce((nextState, model) => { + .forEach((model) => { const flag = CRDs[referenceForModel(model)]; // eslint-disable-next-line no-console console.log(`${flag} was detected.`); + flagUpdates[flag] = true; + }); - return nextState.set(flag, true); - }, state); + return { ...state, ...flagUpdates }; + } default: return state; } }; -export const getFlagsObject = ({ [featureReducerName]: featureState }: RootState): FlagsObject => - featureState.toObject(); +export const getFlagsObject = ({ [featureReducerName]: featureState }: RootState): FlagsObject => ({ + ...featureState, +}); export type FlagsObject = { [key: string]: boolean }; diff --git a/frontend/public/reducers/observe.ts b/frontend/public/reducers/observe.ts index 7d30486a54d..d696a383c53 100644 --- a/frontend/public/reducers/observe.ts +++ b/frontend/public/reducers/observe.ts @@ -1,4 +1,3 @@ -import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import * as _ from 'lodash'; import type { Alert } from '@console/dynamic-plugin-sdk'; import { AlertStates, RuleStates, SilenceStates } from '@console/dynamic-plugin-sdk'; @@ -10,14 +9,15 @@ const MONITORING_DASHBOARDS_DEFAULT_TIMESPAN = 30 * 60 * 1000; const MONITORING_DASHBOARDS_VARIABLE_ALL_OPTION_KEY = 'ALL_OPTION_KEY'; -export type ObserveState = ImmutableMap; +export type ObserveState = Record; -const newQueryBrowserQuery = (): ImmutableMap => - ImmutableMap({ - id: _.uniqueId('query-browser-query'), - isEnabled: true, - isExpanded: true, - }); +type QueryBrowserQuery = Record; + +const newQueryBrowserQuery = (): QueryBrowserQuery => ({ + id: _.uniqueId('query-browser-query'), + isEnabled: true, + isExpanded: true, +}); const silenceFiringAlerts = (firingAlerts, silences) => { // For each firing alert, store a list of the Silences that are silencing it @@ -46,76 +46,146 @@ const silenceFiringAlerts = (firingAlerts, silences) => { }); }; +const updateQuery = ( + state: ObserveState, + index: number, + updater: (q: QueryBrowserQuery) => QueryBrowserQuery, +): ObserveState => { + const queries = [...state.queryBrowser.queries]; + queries[index] = updater(queries[index]); + return { ...state, queryBrowser: { ...state.queryBrowser, queries } }; +}; + +const mapQueries = ( + state: ObserveState, + mapper: (q: QueryBrowserQuery) => QueryBrowserQuery, +): ObserveState => ({ + ...state, + queryBrowser: { + ...state.queryBrowser, + queries: state.queryBrowser.queries.map(mapper), + }, +}); + export default (state: ObserveState, action: ObserveAction): ObserveState => { if (!state) { - return ImmutableMap({ - dashboards: ImmutableMap({ - dev: ImmutableMap({ + return { + dashboards: { + dev: { endTime: null, pollInterval: 30 * 1000, timespan: MONITORING_DASHBOARDS_DEFAULT_TIMESPAN, - variables: ImmutableMap(), - }), - admin: ImmutableMap({ + variables: {}, + }, + admin: { endTime: null, pollInterval: 30 * 1000, timespan: MONITORING_DASHBOARDS_DEFAULT_TIMESPAN, - variables: ImmutableMap(), - }), - }), - queryBrowser: ImmutableMap({ + variables: {}, + }, + }, + queryBrowser: { metrics: [], pollInterval: null, - queries: ImmutableList([newQueryBrowserQuery()]), + queries: [newQueryBrowserQuery()], timespan: MONITORING_DASHBOARDS_DEFAULT_TIMESPAN, - }), - }); + }, + }; } const queryBrowserPatchQueryHelper = (index: number, patch: { [key: string]: unknown }) => { - const query = state.hasIn(['queryBrowser', 'queries', index]) - ? ImmutableMap(patch) - : newQueryBrowserQuery().merge(patch); - return state.mergeIn(['queryBrowser', 'queries', index], query); + const existing = state.queryBrowser.queries[index]; + const query = existing ? { ...existing, ...patch } : { ...newQueryBrowserQuery(), ...patch }; + const queries = [...state.queryBrowser.queries]; + queries[index] = query; + return { ...state, queryBrowser: { ...state.queryBrowser, queries } }; }; switch (action.type) { - case ActionType.DashboardsPatchVariable: - return state.mergeIn( - ['dashboards', action.payload.perspective, 'variables', action.payload.key], - ImmutableMap(action.payload.patch), - ); - - case ActionType.DashboardsPatchAllVariables: - return state.setIn( - ['dashboards', action.payload.perspective, 'variables'], - ImmutableMap(action.payload.variables), - ); + case ActionType.DashboardsPatchVariable: { + const { perspective, key, patch } = action.payload; + const dashPersp = state.dashboards[perspective]; + return { + ...state, + dashboards: { + ...state.dashboards, + [perspective]: { + ...dashPersp, + variables: { + ...dashPersp.variables, + [key]: { ...dashPersp.variables[key], ...patch }, + }, + }, + }, + }; + } + + case ActionType.DashboardsPatchAllVariables: { + const { perspective, variables } = action.payload; + return { + ...state, + dashboards: { + ...state.dashboards, + [perspective]: { + ...state.dashboards[perspective], + variables: { ...variables }, + }, + }, + }; + } case ActionType.DashboardsClearVariables: - return state.setIn(['dashboards', action.payload.perspective, 'variables'], ImmutableMap()); + return { + ...state, + dashboards: { + ...state.dashboards, + [action.payload.perspective]: { + ...state.dashboards[action.payload.perspective], + variables: {}, + }, + }, + }; case ActionType.DashboardsSetEndTime: - return state.setIn( - ['dashboards', action.payload.perspective, 'endTime'], - action.payload.endTime, - ); + return { + ...state, + dashboards: { + ...state.dashboards, + [action.payload.perspective]: { + ...state.dashboards[action.payload.perspective], + endTime: action.payload.endTime, + }, + }, + }; case ActionType.DashboardsSetPollInterval: - return state.setIn( - ['dashboards', action.payload.perspective, 'pollInterval'], - action.payload.pollInterval, - ); + return { + ...state, + dashboards: { + ...state.dashboards, + [action.payload.perspective]: { + ...state.dashboards[action.payload.perspective], + pollInterval: action.payload.pollInterval, + }, + }, + }; case ActionType.DashboardsSetTimespan: - return state.setIn( - ['dashboards', action.payload.perspective, 'timespan'], - action.payload.timespan, - ); + return { + ...state, + dashboards: { + ...state.dashboards, + [action.payload.perspective]: { + ...state.dashboards[action.payload.perspective], + timespan: action.payload.timespan, + }, + }, + }; case ActionType.DashboardsVariableOptionsLoaded: { const { key, newOptions, perspective } = action.payload; - const { options, value } = state.getIn(['dashboards', perspective, 'variables', key]).toJS(); + const variable = state.dashboards[perspective].variables[key]; + const { options, value } = variable; const patch = _.isEqual(options, newOptions) ? { isLoading: false } : { @@ -125,88 +195,114 @@ export default (state: ObserveState, action: ObserveAction): ObserveState => { value === MONITORING_DASHBOARDS_VARIABLE_ALL_OPTION_KEY || newOptions.includes(value) ? value : perspective === 'dev' && key === 'namespace' - ? state.get('activeNamespace') + ? state.activeNamespace : newOptions[0], }; - return state.mergeIn(['dashboards', perspective, 'variables', key], ImmutableMap(patch)); + return { + ...state, + dashboards: { + ...state.dashboards, + [perspective]: { + ...state.dashboards[perspective], + variables: { + ...state.dashboards[perspective].variables, + [key]: { ...variable, ...patch }, + }, + }, + }, + }; } case ActionType.AlertingSetRules: - return state.set(action.payload.key, action.payload.data); + return { ...state, [action.payload.key]: action.payload.data }; case ActionType.AlertingSetData: { const alertsKey = action.payload.data.perspective === 'admin' ? 'alerts' : 'devAlerts'; - const alerts = action.payload.key === alertsKey ? action.payload.data : state.get(alertsKey); - // notificationAlerts used by notification drawer and certain dashboards + const alerts = action.payload.key === alertsKey ? action.payload.data : state[alertsKey]; const notificationAlerts: NotificationAlerts = action.payload.key === 'notificationAlerts' ? action.payload.data - : state.get('notificationAlerts'); + : state.notificationAlerts; const silencesKey = action.payload.data.perspective === 'admin' ? 'silences' : 'devSilences'; const silences = - action.payload.key === silencesKey ? action.payload.data : state.get(silencesKey); + action.payload.key === silencesKey ? action.payload.data : state[silencesKey]; const isAlertFiring = (alert) => alert?.state === AlertStates.Firing || alert?.state === AlertStates.Silenced; const firingAlerts = _.filter(alerts?.data, isAlertFiring); silenceFiringAlerts(firingAlerts, silences); silenceFiringAlerts(_.filter(notificationAlerts?.data, isAlertFiring), silences); - notificationAlerts.data = _.reject(notificationAlerts.data, { state: AlertStates.Silenced }); - // eslint-disable-next-line no-param-reassign - state = state.set(alertsKey, alerts); - // eslint-disable-next-line no-param-reassign - state = state.set('notificationAlerts', notificationAlerts); + const updatedNotificationAlerts = notificationAlerts + ? { + ...notificationAlerts, + data: _.reject(notificationAlerts.data, { state: AlertStates.Silenced }), + } + : notificationAlerts; + + const updated = { + ...state, + [alertsKey]: alerts, + notificationAlerts: updatedNotificationAlerts, + }; - // For each Silence, store a list of the Alerts it is silencing _.each(_.get(silences, 'data'), (s) => { s.firingAlerts = _.filter(firingAlerts, (a) => isSilenced(a, s)); }); - return state.set(silencesKey, silences); + return { ...updated, [silencesKey]: silences }; } case ActionType.ToggleGraphs: - return state.set('hideGraphs', !state.get('hideGraphs')); + return { ...state, hideGraphs: !state.hideGraphs }; case ActionType.QueryBrowserAddQuery: - return state.setIn( - ['queryBrowser', 'queries'], - state.getIn(['queryBrowser', 'queries']).push(newQueryBrowserQuery()), - ); + return { + ...state, + queryBrowser: { + ...state.queryBrowser, + queries: [...state.queryBrowser.queries, newQueryBrowserQuery()], + }, + }; case ActionType.QueryBrowserDuplicateQuery: { const { index } = action.payload; - const originQueryText = state.getIn(['queryBrowser', 'queries', index, 'text']); - const duplicate = newQueryBrowserQuery().merge({ + const originQueryText = state.queryBrowser.queries[index]?.text; + const duplicate = { + ...newQueryBrowserQuery(), text: originQueryText, isEnabled: false, - }); - return state.setIn( - ['queryBrowser', 'queries'], - state.getIn(['queryBrowser', 'queries']).push(duplicate), - ); + }; + return { + ...state, + queryBrowser: { + ...state.queryBrowser, + queries: [...state.queryBrowser.queries, duplicate], + }, + }; } case ActionType.QueryBrowserDeleteAllQueries: - return state.setIn(['queryBrowser', 'queries'], ImmutableList([newQueryBrowserQuery()])); + return { + ...state, + queryBrowser: { ...state.queryBrowser, queries: [newQueryBrowserQuery()] }, + }; - case ActionType.QueryBrowserDeleteAllSeries: { - return state.setIn( - ['queryBrowser', 'queries'], - state.getIn(['queryBrowser', 'queries']).map((q) => q.set('series', undefined)), - ); - } + case ActionType.QueryBrowserDeleteAllSeries: + return mapQueries(state, (q) => ({ ...q, series: undefined })); case ActionType.QueryBrowserDeleteQuery: { - let queries = state.getIn(['queryBrowser', 'queries']).delete(action.payload.index); - if (queries.size === 0) { - queries = queries.push(newQueryBrowserQuery()); + let queries = state.queryBrowser.queries.filter((_q, i) => i !== action.payload.index); + if (queries.length === 0) { + queries = [newQueryBrowserQuery()]; } - return state.setIn(['queryBrowser', 'queries'], queries); + return { ...state, queryBrowser: { ...state.queryBrowser, queries } }; } case ActionType.QueryBrowserDismissNamespaceAlert: - return state.setIn(['queryBrowser', 'dismissNamespaceAlert'], true); + return { + ...state, + queryBrowser: { ...state.queryBrowser, dismissNamespaceAlert: true }, + }; case ActionType.QueryBrowserPatchQuery: { const { index, patch } = action.payload; @@ -214,65 +310,65 @@ export default (state: ObserveState, action: ObserveAction): ObserveState => { } case ActionType.QueryBrowserRunQueries: { - const queries = state.getIn(['queryBrowser', 'queries']).map((q) => { - const isEnabled = q.get('isEnabled'); - const query = q.get('query'); - const text = _.trim(q.get('text')); - return isEnabled && query !== text ? q.merge({ query: text, series: undefined }) : q; + const queries = state.queryBrowser.queries.map((q) => { + const { isEnabled, query, text: rawText } = q; + const text = _.trim(rawText); + return isEnabled && query !== text ? { ...q, query: text, series: undefined } : q; }); - - return state - .setIn(['queryBrowser', 'queries'], queries) - .setIn(['queryBrowser', 'lastRequestTime'], Date.now()); + return { + ...state, + queryBrowser: { ...state.queryBrowser, queries, lastRequestTime: Date.now() }, + }; } - case ActionType.QueryBrowserSetAllExpanded: { - const queries = state - .getIn(['queryBrowser', 'queries']) - .map((q) => q.set('isExpanded', action.payload.isExpanded)); - return state.setIn(['queryBrowser', 'queries'], queries); - } + case ActionType.QueryBrowserSetAllExpanded: + return mapQueries(state, (q) => ({ ...q, isExpanded: action.payload.isExpanded })); case ActionType.QueryBrowserSetMetrics: - return state.setIn(['queryBrowser', 'metrics'], action.payload.metrics); + return { + ...state, + queryBrowser: { ...state.queryBrowser, metrics: action.payload.metrics }, + }; case ActionType.QueryBrowserSetPollInterval: - return state.setIn(['queryBrowser', 'pollInterval'], action.payload.pollInterval); + return { + ...state, + queryBrowser: { ...state.queryBrowser, pollInterval: action.payload.pollInterval }, + }; case ActionType.QueryBrowserSetTimespan: - return state.setIn(['queryBrowser', 'timespan'], action.payload.timespan); + return { + ...state, + queryBrowser: { ...state.queryBrowser, timespan: action.payload.timespan }, + }; case ActionType.QueryBrowserToggleAllSeries: { const { index } = action.payload; - const isDisabledSeriesEmpty = _.isEmpty( - state.getIn(['queryBrowser', 'queries', index, 'disabledSeries']), - ); - const series = state.getIn(['queryBrowser', 'queries', index, 'series']); - const patch = { disabledSeries: isDisabledSeriesEmpty ? series : [] }; + const query = state.queryBrowser.queries[index]; + const isDisabledSeriesEmpty = _.isEmpty(query?.disabledSeries); + const patch = { disabledSeries: isDisabledSeriesEmpty ? query?.series : [] }; return queryBrowserPatchQueryHelper(index, patch); } case ActionType.QueryBrowserToggleIsEnabled: { - const query = state.getIn(['queryBrowser', 'queries', action.payload.index]); - const isEnabled = !query.get('isEnabled'); - return state.setIn( - ['queryBrowser', 'queries', action.payload.index], - query.merge({ - isEnabled, - isExpanded: isEnabled, - query: isEnabled ? query.get('text') : '', - }), - ); + const query = state.queryBrowser.queries[action.payload.index]; + const isEnabled = !query.isEnabled; + return updateQuery(state, action.payload.index, () => ({ + ...query, + isEnabled, + isExpanded: isEnabled, + query: isEnabled ? query.text : '', + })); } case ActionType.QueryBrowserToggleSeries: - return state.updateIn( - ['queryBrowser', 'queries', action.payload.index, 'disabledSeries'], - (v) => _.xorWith(v, [action.payload.labels], _.isEqual), - ); + return updateQuery(state, action.payload.index, (q) => ({ + ...q, + disabledSeries: _.xorWith(q.disabledSeries, [action.payload.labels], _.isEqual), + })); case ActionType.SetAlertCount: - return state.set('alertCount', action.payload.alertCount); + return { ...state, alertCount: action.payload.alertCount }; default: break; diff --git a/frontend/public/reducers/ui.ts b/frontend/public/reducers/ui.ts index 8afc7fff3f0..380be19a468 100644 --- a/frontend/public/reducers/ui.ts +++ b/frontend/public/reducers/ui.ts @@ -1,4 +1,3 @@ -import { Map as ImmutableMap } from 'immutable'; import * as _ from 'lodash'; import { getUser } from '@console/dynamic-plugin-sdk'; import { ALL_APPLICATIONS_KEY, ALL_NAMESPACES_KEY } from '@console/shared/src/constants/common'; @@ -8,14 +7,12 @@ import { OverviewSpecialGroup } from '../components/overview/constants'; import { getNamespace } from '../components/utils/link'; import type { RootState } from '../redux'; -export type UIState = ImmutableMap; - -const NOTIFICATION_DRAWER_EXPANDED_PATH = ['notifications', 'isExpanded']; +export type UIState = Record; export default (state: UIState, action: UIAction): UIState => { if (!state) { const { pathname } = window.location; - return ImmutableMap({ + return { activeNavSectionId: 'workloads', location: pathname, showOperandsInAllNamespaces: true, @@ -23,39 +20,39 @@ export default (state: UIState, action: UIAction): UIState => { activeApplication: ALL_APPLICATIONS_KEY, pluginCSPViolations: {}, createProjectMessage: '', - serviceLevel: ImmutableMap({ + serviceLevel: { level: '', daysRemaining: null, trialDateEnd: null, hasSecretAccess: false, clusterID: '', - }), - overview: ImmutableMap({ + }, + overview: { metrics: {}, - resources: ImmutableMap({}), + resources: {}, selectedDetailsTab: 'Resources', selectedUID: '', selectedGroup: OverviewSpecialGroup.GROUP_BY_APPLICATION, - groupOptions: ImmutableMap(), + groupOptions: {}, filterValue: '', - }), + }, user: {}, - utilizationDuration: ImmutableMap({ + utilizationDuration: { duration: null, endTime: null, selectedKey: null, - }), - deprecatedOperator: ImmutableMap({ + }, + deprecatedOperator: { package: null, channel: null, version: null, - }), - }); + }, + }; } switch (action.type) { case ActionType.SetActiveApplication: - return state.set('activeApplication', action.payload.application); + return { ...state, activeApplication: action.payload.application }; case ActionType.SetActiveNamespace: if (!action.payload.namespace) { @@ -64,99 +61,175 @@ export default (state: UIState, action: UIAction): UIState => { return state; } - return state.set('activeNamespace', action.payload.namespace); + return { ...state, activeNamespace: action.payload.namespace }; case ActionType.SetCurrentLocation: { - // eslint-disable-next-line no-param-reassign - state = state.set('location', action.payload.location); + const updated = { ...state, location: action.payload.location }; const ns = getNamespace(action.payload.location); if (_.isUndefined(ns)) { - return state; + return updated; } - return state.set('activeNamespace', ns); + return { ...updated, activeNamespace: ns }; } case ActionType.SetServiceLevel: - return state.set('serviceLevel', { - level: action.payload.serviceLevel, - daysRemaining: action.payload.daysRemaining, - clusterID: action.payload.clusterID, - trialDateEnd: action.payload.trialDateEnd, - hasSecretAccess: action.payload.hasSecretAccess, - }); + return { + ...state, + serviceLevel: { + level: action.payload.serviceLevel, + daysRemaining: action.payload.daysRemaining, + clusterID: action.payload.clusterID, + trialDateEnd: action.payload.trialDateEnd, + hasSecretAccess: action.payload.hasSecretAccess, + }, + }; case ActionType.SortList: - return state.mergeIn( - ['listSorts', action.payload.listId], - _.pick(action.payload, ['field', 'func', 'orderBy']), - ); + return { + ...state, + listSorts: { + ...state.listSorts, + [action.payload.listId]: { + ...state.listSorts?.[action.payload.listId], + ..._.pick(action.payload, ['field', 'func', 'orderBy']), + }, + }, + }; case ActionType.SetCreateProjectMessage: - return state.set('createProjectMessage', action.payload.message); + return { ...state, createProjectMessage: action.payload.message }; case ActionType.SetClusterID: - return state.set('clusterID', action.payload.clusterID); + return { ...state, clusterID: action.payload.clusterID }; case ActionType.NotificationDrawerToggleExpanded: - return state.setIn( - NOTIFICATION_DRAWER_EXPANDED_PATH, - !state.getIn(NOTIFICATION_DRAWER_EXPANDED_PATH), - ); + return { + ...state, + notifications: { + ...state.notifications, + isExpanded: !state.notifications?.isExpanded, + }, + }; case ActionType.SelectOverviewItem: - return state.setIn(['overview', 'selectedUID'], action.payload.uid); + return { + ...state, + overview: { ...state.overview, selectedUID: action.payload.uid }, + }; case ActionType.SelectOverviewDetailsTab: - return state.setIn(['overview', 'selectedDetailsTab'], action.payload.tab); + return { + ...state, + overview: { ...state.overview, selectedDetailsTab: action.payload.tab }, + }; case ActionType.DismissOverviewDetails: - return state.mergeIn(['overview'], { selectedUID: '', selectedDetailsTab: '' }); + return { + ...state, + overview: { ...state.overview, selectedUID: '', selectedDetailsTab: '' }, + }; case ActionType.UpdateOverviewMetrics: - return state.setIn(['overview', 'metrics'], action.payload.metrics); + return { + ...state, + overview: { ...state.overview, metrics: action.payload.metrics }, + }; case ActionType.UpdateOverviewResources: { - const newResources = ImmutableMap(_.keyBy(action.payload.resources, 'obj.metadata.uid')); - return state.setIn(['overview', 'resources'], newResources); + const newResources = _.keyBy(action.payload.resources, 'obj.metadata.uid'); + return { + ...state, + overview: { ...state.overview, resources: newResources }, + }; } - case ActionType.UpdateOverviewSelectedGroup: { - return state.setIn(['overview', 'selectedGroup'], action.payload.group); - } + case ActionType.UpdateOverviewSelectedGroup: + return { + ...state, + overview: { ...state.overview, selectedGroup: action.payload.group }, + }; - case ActionType.UpdateOverviewLabels: { - return state.setIn(['overview', 'labels'], action.payload.labels); - } + case ActionType.UpdateOverviewLabels: + return { + ...state, + overview: { ...state.overview, labels: action.payload.labels }, + }; + + case ActionType.UpdateOverviewFilterValue: + return { + ...state, + overview: { ...state.overview, filterValue: action.payload.value }, + }; - case ActionType.UpdateOverviewFilterValue: { - return state.setIn(['overview', 'filterValue'], action.payload.value); - } case ActionType.SetPodMetrics: - return state.setIn(['metrics', 'pod'], action.payload.podMetrics); + return { + ...state, + metrics: { ...state.metrics, pod: action.payload.podMetrics }, + }; case ActionType.SetNamespaceMetrics: - return state.setIn(['metrics', 'namespace'], action.payload.namespaceMetrics); + return { + ...state, + metrics: { ...state.metrics, namespace: action.payload.namespaceMetrics }, + }; case ActionType.SetNodeMetrics: - return state.setIn(['metrics', 'node'], action.payload.nodeMetrics); + return { + ...state, + metrics: { ...state.metrics, node: action.payload.nodeMetrics }, + }; case ActionType.SetPVCMetrics: - return state.setIn(['metrics', 'pvc'], action.payload.pvcMetrics); + return { + ...state, + metrics: { ...state.metrics, pvc: action.payload.pvcMetrics }, + }; case ActionType.SetUtilizationDuration: - return state.setIn(['utilizationDuration', 'duration'], action.payload.duration); + return { + ...state, + utilizationDuration: { + ...state.utilizationDuration, + duration: action.payload.duration, + }, + }; case ActionType.SetUtilizationDurationSelectedKey: - return state.setIn(['utilizationDuration', 'selectedKey'], action.payload.key); + return { + ...state, + utilizationDuration: { + ...state.utilizationDuration, + selectedKey: action.payload.key, + }, + }; case ActionType.SetUtilizationDurationEndTime: - return state.setIn(['utilizationDuration', 'endTime'], action.payload.endTime); + return { + ...state, + utilizationDuration: { + ...state.utilizationDuration, + endTime: action.payload.endTime, + }, + }; case ActionType.SetShowOperandsInAllNamespaces: - return state.set('showOperandsInAllNamespaces', action.payload.value); + return { ...state, showOperandsInAllNamespaces: action.payload.value }; case ActionType.SetDeprecatedPackage: - return state.setIn(['deprecatedOperator', 'package'], action.payload.value); + return { + ...state, + deprecatedOperator: { ...state.deprecatedOperator, package: action.payload.value }, + }; case ActionType.SetDeprecatedChannel: - return state.setIn(['deprecatedOperator', 'channel'], action.payload.value); + return { + ...state, + deprecatedOperator: { ...state.deprecatedOperator, channel: action.payload.value }, + }; case ActionType.SetDeprecatedVersion: - return state.setIn(['deprecatedOperator', 'version'], action.payload.value); + return { + ...state, + deprecatedOperator: { ...state.deprecatedOperator, version: action.payload.value }, + }; case ActionType.SetPluginCSPViolations: - return state.mergeIn(['pluginCSPViolations'], { - [action.payload.pluginName]: action.payload.hasViolation, - }); + return { + ...state, + pluginCSPViolations: { + ...state.pluginCSPViolations, + [action.payload.pluginName]: action.payload.hasViolation, + }, + }; default: break; } @@ -165,9 +238,9 @@ export default (state: UIState, action: UIAction): UIState => { export const userStateToProps = (state: RootState) => ({ user: getUser(state) }); -export const getActiveNamespace = ({ UI }: RootState): string => UI.get('activeNamespace'); +export const getActiveNamespace = ({ UI }: RootState): string => UI.activeNamespace; -export const getActiveApplication = ({ UI }: RootState): string => UI.get('activeApplication'); +export const getActiveApplication = ({ UI }: RootState): string => UI.activeApplication; export const isNotificationDrawerExpanded = ({ UI }: RootState): boolean => - !!UI.getIn(NOTIFICATION_DRAWER_EXPANDED_PATH); + !!UI.notifications?.isExpanded; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 6dc1dd337e0..fb8a52997db 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -12833,13 +12833,6 @@ __metadata: languageName: node linkType: hard -"immutable@npm:^3.8.3": - version: 3.8.3 - resolution: "immutable@npm:3.8.3" - checksum: 10c0/bafa7b8371b7622bc3d128cd9e6bba3a654b968f09a237929629f43ac26f7e974a5879cd38baad0c26f6f0628753968611bf832add7bf0c44d647bf4306a2988 - languageName: node - linkType: hard - "import-fresh@npm:^3.1.0, import-fresh@npm:^3.2.1": version: 3.3.1 resolution: "import-fresh@npm:3.3.1" @@ -16575,7 +16568,6 @@ __metadata: i18next-http-backend: "npm:^4.0.1" i18next-pseudo: "npm:^2.2.1" i18next-v4-format-converter: "npm:^1.1.2" - immutable: "npm:^3.8.3" istextorbinary: "npm:^9.5.0" jest: "npm:^30.4.2" jest-cli: "npm:^30.4.2"