From 82af86a20440220c3d6822dcd35dfff89c883676 Mon Sep 17 00:00:00 2001 From: Robb Hamilton Date: Mon, 9 Mar 2026 15:18:44 -0400 Subject: [PATCH] CONSOLE-4447: Migrate modals to PatternFly 6 Modal components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses code review feedback with fixes for accessibility, error handling, i18n, null safety, and code quality: Accessibility improvements: - Add aria-labelledby to Modal components linking to ModalHeader labelId - Fix FormGroup in cluster-update-modal to use role="radiogroup" instead of unused fieldId - Add aria-label to TextInput components in taints-modal - Add isDisabled to delete button while operation in progress Error handling: - Add .catch(() => {}) to promise chains in cluster-update-modal and rollback-modal i18n fixes: - Add missing 'public~' namespace prefix to Close button translation - Translate Tooltip content in taints-modal Null safety: - Add defensive fallback for semver.parse in cluster-channel-modal using nullish coalescing Code quality: - Fix duplicate modal rendering in cluster-settings using useRef to track modal state - Use useQueryParamsMutator hook's getQueryArgument for URL params - Replace raw input elements with PatternFly TextInput components in taints-modal - Fix inconsistent error checking in rollback-modal - Fix typo: defalt-column-management → default-column-management - Migrate taints-modal to lazy loading pattern (LazyTaintsModalOverlay) Co-Authored-By: Claude Sonnet 4.5 --- .../src/actions/hooks/useCommonActions.ts | 4 +- .../cluster-settings/cluster-settings.tsx | 20 +- .../column-management-modal.spec.tsx | 2 + .../modals/cluster-channel-modal.tsx | 168 ++++++---- .../modals/cluster-more-updates-modal.tsx | 86 ++--- .../modals/cluster-update-modal.tsx | 305 ++++++++++-------- .../modals/column-management-modal.tsx | 156 +++++---- .../modals/configure-count-modal.tsx | 7 +- .../components/modals/delete-pvc-modal.tsx | 87 +++-- .../public/components/modals/error-modal.tsx | 42 +-- frontend/public/components/modals/index.ts | 7 + .../components/modals/rollback-modal.tsx | 95 ++++-- .../public/components/modals/taints-modal.tsx | 205 +++++++----- .../components/modals/tolerations-modal.tsx | 282 ++++++++-------- 14 files changed, 830 insertions(+), 636 deletions(-) diff --git a/frontend/packages/console-app/src/actions/hooks/useCommonActions.ts b/frontend/packages/console-app/src/actions/hooks/useCommonActions.ts index 7f8c45096b6..51e1414d964 100644 --- a/frontend/packages/console-app/src/actions/hooks/useCommonActions.ts +++ b/frontend/packages/console-app/src/actions/hooks/useCommonActions.ts @@ -7,10 +7,10 @@ import { LazyAnnotationsModalOverlay, LazyDeleteModalOverlay, LazyLabelsModalOverlay, + LazyTaintsModalOverlay, LazyTolerationsModalOverlay, } from '@console/internal/components/modals'; import { useConfigureCountModal } from '@console/internal/components/modals/configure-count-modal'; -import { TaintsModalOverlay } from '@console/internal/components/modals/taints-modal'; import { asAccessReview } from '@console/internal/components/utils/rbac'; import { resourceObjPath } from '@console/internal/components/utils/resource-link'; import type { K8sModel, K8sResourceKind, NodeKind } from '@console/internal/module/k8s'; @@ -143,7 +143,7 @@ export const useCommonActions = ( id: 'edit-taints', label: t('console-app~Edit taints'), cta: () => - launchModal(TaintsModalOverlay, { + launchModal(LazyTaintsModalOverlay, { resourceKind: kind, resource: resource as NodeKind, }), diff --git a/frontend/public/components/cluster-settings/cluster-settings.tsx b/frontend/public/components/cluster-settings/cluster-settings.tsx index 817fdcfd637..ac3a4c2e7e2 100644 --- a/frontend/public/components/cluster-settings/cluster-settings.tsx +++ b/frontend/public/components/cluster-settings/cluster-settings.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import type { FC, ReactNode } from 'react'; -import { useEffect } from 'react'; +import { useEffect, useRef, useMemo } from 'react'; import * as _ from 'lodash'; import { css } from '@patternfly/react-styles'; import * as semver from 'semver'; @@ -888,7 +888,7 @@ export const ClusterVersionDetailsTable: FC = ( obj: cv, autoscalers, }) => { - const { removeQueryArgument } = useQueryParamsMutator(); + const { getQueryArgument, removeQueryArgument } = useQueryParamsMutator(); const { history = [] } = cv.status; const clusterID = getClusterID(cv); const desiredImage: string = _.get(cv, 'status.desired.image') || ''; @@ -908,16 +908,26 @@ export const ClusterVersionDetailsTable: FC = ( const updateStartedTime = getStartedTimeForCVDesiredVersion(cv, desiredVersion); const workerMachineConfigPool = getMCPByName(machineConfigPools, NodeTypes.worker); const launchModal = useOverlay(); + const modalOpenedRef = useRef(false); + + // Check URL params once to avoid re-reading on every cv change + const hasShowVersions = useMemo(() => !!getQueryArgument('showVersions'), [getQueryArgument]); + const hasShowChannels = useMemo(() => !!getQueryArgument('showChannels'), [getQueryArgument]); useEffect(() => { - if (new URLSearchParams(window.location.search).has('showVersions')) { + if (modalOpenedRef.current) { + return; + } + if (hasShowVersions) { launchModal(LazyClusterUpdateModalOverlay, { cv }); removeQueryArgument('showVersions'); - } else if (new URLSearchParams(window.location.search).has('showChannels')) { + modalOpenedRef.current = true; + } else if (hasShowChannels) { launchModal(LazyClusterChannelModalOverlay, { cv }); removeQueryArgument('showChannels'); + modalOpenedRef.current = true; } - }, [launchModal, cv, removeQueryArgument]); + }, [launchModal, cv, removeQueryArgument, hasShowVersions, hasShowChannels]); return ( <> diff --git a/frontend/public/components/modals/__tests__/column-management-modal.spec.tsx b/frontend/public/components/modals/__tests__/column-management-modal.spec.tsx index 69d75cf08f5..24f8b873760 100644 --- a/frontend/public/components/modals/__tests__/column-management-modal.spec.tsx +++ b/frontend/public/components/modals/__tests__/column-management-modal.spec.tsx @@ -141,6 +141,7 @@ describe('ColumnManagementModal component', () => { }, []), ), type: columnManagementType, + showNamespaceOverride: true, }} userSettingState={null} setUserSettingState={jest.fn()} @@ -205,6 +206,7 @@ describe('ColumnManagementModal component', () => { id: columnManagementID, selectedColumns: new Set(modifiedColumns), type: columnManagementType, + showNamespaceOverride: true, }} userSettingState={null} setUserSettingState={jest.fn()} diff --git a/frontend/public/components/modals/cluster-channel-modal.tsx b/frontend/public/components/modals/cluster-channel-modal.tsx index 28a42666ec0..9e33425d404 100644 --- a/frontend/public/components/modals/cluster-channel-modal.tsx +++ b/frontend/public/components/modals/cluster-channel-modal.tsx @@ -1,7 +1,21 @@ import type { FormEventHandler } from 'react'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Content, TextInput, ContentVariants } from '@patternfly/react-core'; +import { + Button, + Content, + ContentVariants, + Form, + FormGroup, + FormHelperText, + HelperText, + HelperTextItem, + Modal, + ModalBody, + ModalHeader, + ModalVariant, + TextInput, +} from '@patternfly/react-core'; import * as semver from 'semver'; import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; @@ -9,13 +23,7 @@ import { ChannelDocLink } from '../cluster-settings/cluster-settings'; import { ClusterVersionModel } from '../../models'; import { ConsoleSelect } from '@console/internal/components/utils/console-select'; import { isManaged } from '../utils/documentation'; -import { - ModalBody, - ModalComponentProps, - ModalSubmitFooter, - ModalTitle, - ModalWrapper, -} from '../factory/modal'; +import { ModalComponentProps } from '../factory/modal'; import { ClusterVersionKind, getAvailableClusterChannels, @@ -23,8 +31,9 @@ import { k8sPatch, } from '../../module/k8s'; import { usePromiseHandler } from '@console/shared/src/hooks/promise-handler'; +import { ModalFooterWithAlerts } from '@console/shared/src/components/modals/ModalFooterWithAlerts'; -const ClusterChannelModal = (props: ClusterChannelModalProps) => { +export const ClusterChannelModal = (props: ClusterChannelModalProps) => { const { cancel, close, cv } = props; const [handlePromise, inProgress, errorMessage] = usePromiseHandler(); const [channel, setChannel] = useState(cv.spec.channel); @@ -34,6 +43,8 @@ const ClusterChannelModal = (props: ClusterChannelModalProps) => { return o; }, {}); const version = semver.parse(getLastCompletedUpdate(cv)); + const versionMajor = version?.major ?? 4; + const versionMinor = version?.minor ?? 0; const channelsExist = cv.status?.desired?.channels?.length; const submit: FormEventHandler = (e): void => { e.preventDefault(); @@ -44,76 +55,97 @@ const ClusterChannelModal = (props: ClusterChannelModalProps) => { }; return ( -
- - {channelsExist ? t('public~Select channel') : t('public~Input channel')} - + <> + - - - {channelsExist - ? t( - 'public~The current version is available in the channels listed in the dropdown below. Select a channel that reflects the desired version. Critical security updates will be delivered to any vulnerable channels.', - ) - : t( - 'public~Input a channel that reflects the desired version. To verify if the version exists in a channel, save and check the update status. Critical security updates will be delivered to any vulnerable channels.', - )} - - {!isManaged() && ( - - + + + + {channelsExist + ? t( + 'public~The current version is available in the channels listed in the dropdown below. Select a channel that reflects the desired version. Critical security updates will be delivered to any vulnerable channels.', + ) + : t( + 'public~Input a channel that reflects the desired version. To verify if the version exists in a channel, save and check the update status. Critical security updates will be delivered to any vulnerable channels.', + )} - )} - -
- - {channelsExist ? ( - setChannel(newChannel)} - selectedKey={channel} - title={t('public~Channel')} - /> - ) : ( - <> - + + + )} + + + {channelsExist ? ( + setChannel(newChannel)} - value={channel} - placeholder={t(`public~e.g., {{version}}`, { - version: `stable-${version.major}.${version.minor}`, - })} - data-test="channel-modal-input" + items={availableChannels} + onChange={(newChannel: string) => setChannel(newChannel)} + selectedKey={channel} + title={t('public~Channel')} /> -

- {t(`public~Potential channels are {{stable}}, {{fast}}, or {{candidate}}.`, { - stable: `stable-${version.major}.${version.minor}`, - fast: `fast-${version.major}.${version.minor}`, - candidate: `candidate-${version.major}.${version.minor}`, - })} -

- - )} -
+ ) : ( + <> + setChannel(newChannel)} + value={channel} + placeholder={t(`public~e.g., {{version}}`, { + version: `stable-${versionMajor}.${versionMinor}`, + })} + data-test="channel-modal-input" + /> + + + + {t(`public~Potential channels are {{stable}}, {{fast}}, or {{candidate}}.`, { + stable: `stable-${versionMajor}.${versionMinor}`, + fast: `fast-${versionMajor}.${versionMinor}`, + candidate: `candidate-${versionMajor}.${versionMinor}`, + })} + + + + + )} + +
- - + + + + + ); }; export const ClusterChannelModalOverlay: OverlayComponent = (props) => { return ( - + - + ); }; diff --git a/frontend/public/components/modals/cluster-more-updates-modal.tsx b/frontend/public/components/modals/cluster-more-updates-modal.tsx index 45a3cadc550..8246b8c5f5e 100644 --- a/frontend/public/components/modals/cluster-more-updates-modal.tsx +++ b/frontend/public/components/modals/cluster-more-updates-modal.tsx @@ -1,6 +1,7 @@ import type { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { ActionGroup, Button } from '@patternfly/react-core'; +import { Button, Modal, ModalBody, ModalHeader, ModalVariant } from '@patternfly/react-core'; +import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; import { isClusterExternallyManaged } from '@console/shared/src/hooks/useCanClusterUpgrade'; @@ -13,18 +14,13 @@ import { isMinorVersionNewer, showReleaseNotes, } from '../../module/k8s'; -import { - ModalBody, - ModalComponentProps, - ModalFooter, - ModalTitle, - ModalWrapper, -} from '../factory/modal'; +import { ModalComponentProps } from '../factory/modal'; import { ClusterNotUpgradeableAlert, UpdateBlockedLabel, } from '../cluster-settings/cluster-settings'; import { ReleaseNotesLink } from '../utils/release-notes-link'; +import { ModalFooterWithAlerts } from '@console/shared/src/components/modals/ModalFooterWithAlerts'; export const ClusterMoreUpdatesModal: FC = ({ cancel, cv }) => { const availableUpdates = getSortedAvailableUpdates(cv); @@ -35,58 +31,60 @@ export const ClusterMoreUpdatesModal: FC = ({ canc const { t } = useTranslation(); return ( -
- {t('public~Other available paths')} + <> + {clusterUpgradeableFalseAndNotExternallyManaged && ( )} - - - - - {releaseNotes && } - - - +
{t('public~Version')}{t('public~Release notes')}
+ + + + {releaseNotes && } + + + {moreAvailableUpdates.map((update) => { return ( - - + + {releaseNotes && ( - + )} - + ); })} - -
{t('public~Version')}{t('public~Release notes')}
+
{update.version} {clusterUpgradeableFalseAndNotExternallyManaged && isMinorVersionNewer(getLastCompletedUpdate(cv), update.version) && ( )} - + {getReleaseNotesLink(update.version) ? ( ) : ( '-' )} -
+ +
- - - - - -
+ + + + ); }; @@ -94,9 +92,15 @@ export const ClusterMoreUpdatesModalOverlay: OverlayComponent { return ( - + - + ); }; diff --git a/frontend/public/components/modals/cluster-update-modal.tsx b/frontend/public/components/modals/cluster-update-modal.tsx index 9f23ced5964..2f2643f95b8 100644 --- a/frontend/public/components/modals/cluster-update-modal.tsx +++ b/frontend/public/components/modals/cluster-update-modal.tsx @@ -2,7 +2,19 @@ import * as _ from 'lodash'; import type { FormEvent, FormEventHandler } from 'react'; import { useState, useEffect, useCallback, Fragment } from 'react'; import { useTranslation } from 'react-i18next'; -import { Alert, Radio, Content, ContentVariants } from '@patternfly/react-core'; +import { + Alert, + Button, + Content, + ContentVariants, + Form, + FormGroup, + Modal, + ModalBody, + ModalHeader, + ModalVariant, + Radio, +} from '@patternfly/react-core'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; import { DropdownWithSwitch } from '@console/shared/src/components/dropdown'; @@ -28,18 +40,13 @@ import { sortMCPsByCreationTimestamp, } from '../../module/k8s'; import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; -import { - ModalWrapper, - ModalBody, - ModalComponentProps, - ModalSubmitFooter, - ModalTitle, -} from '../factory/modal'; +import { ModalComponentProps } from '../factory/modal'; import { ClusterNotUpgradeableAlert, UpdateBlockedLabel, } from '../cluster-settings/cluster-settings'; import { MachineConfigPoolsSelector } from '../machine-config-pools-selector'; +import { ModalFooterWithAlerts } from '@console/shared/src/components/modals/ModalFooterWithAlerts'; enum upgradeTypes { Full = 'Full', @@ -147,7 +154,9 @@ const ClusterUpdateModal = (props: ClusterUpdateModalProps) => { ...MCPsToResumePromises, ...MCPsToPausePromises, ]), - ).then(() => close()); + ) + .then(() => close()) + .catch(() => {}); }, [ desiredRecommendedUpdate, @@ -199,151 +208,173 @@ const ClusterUpdateModal = (props: ClusterUpdateModalProps) => { } return ( -
- {t('public~Update cluster')} + <> + - {clusterUpgradeableFalse && } -
- -

{currentVersion}

-
-
- - + {clusterUpgradeableFalse && } + +

{currentVersion}

+
+ + + {t('public~Include versions with known issues')} + + {t( + 'public~These versions are supported, but include known issues. Review the known issues before updating.', + )} + + + } + switchLabelIsReversed + switchOnChange={(val) => setIncludeNotRecommended(val)} + toggleLabel={desiredVersion} + /> + {desiredNotRecommendedUpdate && desiredNotRecommendedUpdateConditions?.message && ( + + + + + {desiredNotRecommendedUpdateConditions.message.split('\n').map((item) => ( + + {item} +
+
+ ))} +
+
+
+
+ )} +
+ - {t('public~Include versions with known issues')} + {t('public~Update options')} {t( - 'public~These versions are supported, but include known issues. Review the known issues before updating.', + 'public~Full cluster update allows you to update all your Nodes, but takes longer. Control plane only update allows you to pause worker and custom pool Nodes to accommodate your maintenance schedule.', )} } - switchLabelIsReversed - switchOnChange={(val) => setIncludeNotRecommended(val)} - toggleLabel={desiredVersion} - /> - {desiredNotRecommendedUpdate && desiredNotRecommendedUpdateConditions?.message && ( - + handleUpgradeTypeChange(upgradeTypes.Full)} + label={t('public~Full cluster update')} + id={upgradeTypes.Full} + value={upgradeTypes.Full} + description={t( + 'public~{{master}}, {{worker}}, and custom pool {{resource}} are updated concurrently. This might take longer, so make sure to allocate enough time for maintenance.', + { + master: NodeTypeNames.Master, + worker: NodeTypeNames.Worker, + resource: NodeModel.labelPlural, + }, )} - variant="info" - data-test="update-cluster-modal-not-recommended-alert" - > - - - - {desiredNotRecommendedUpdateConditions.message.split('\n').map((item) => ( - - {item} -
-
- ))} -
-
-
-
- )} -
-
- - handleUpgradeTypeChange(upgradeTypes.Full)} - label={t('public~Full cluster update')} - id={upgradeTypes.Full} - value={upgradeTypes.Full} - description={t( - 'public~{{master}}, {{worker}}, and custom pool {{resource}} are updated concurrently. This might take longer, so make sure to allocate enough time for maintenance.', - { - master: NodeTypeNames.Master, - worker: NodeTypeNames.Worker, - resource: NodeModel.labelPlural, - }, - )} - className="pf-v6-u-mb-sm" - body={ - machineConfigPoolsLoaded && - pausedMCPs.length > 0 && - upgradeType === upgradeTypes.Full && ( - - ) - } - data-test="update-cluster-modal-full-update-radio" - /> - handleUpgradeTypeChange(upgradeTypes.Partial)} - label={t('public~Control plane only update')} - id={upgradeTypes.Partial} - value={upgradeTypes.Partial} - description={t( - 'public~Pause {{worker}} or custom pool {{resource}} updates to accommodate your maintenance schedule.', - { worker: NodeTypeNames.Worker, resource: NodeModel.label }, - )} - className="pf-v6-u-mb-md" - body={ - upgradeType === upgradeTypes.Partial && ( - - ) - } - data-test="update-cluster-modal-partial-update-radio" - /> -
+ className="pf-v6-u-mb-md" + body={ + upgradeType === upgradeTypes.Partial && ( + + ) + } + data-test="update-cluster-modal-partial-update-radio" + /> + +
- - + + + + + ); }; export const ClusterUpdateModalOverlay: OverlayComponent = (props) => { return ( - + - + ); }; diff --git a/frontend/public/components/modals/column-management-modal.tsx b/frontend/public/components/modals/column-management-modal.tsx index 0fe16ac6fc3..c09fa1e51dd 100644 --- a/frontend/public/components/modals/column-management-modal.tsx +++ b/frontend/public/components/modals/column-management-modal.tsx @@ -2,14 +2,20 @@ import type { FC, SyntheticEvent } from 'react'; import { useState } from 'react'; import { Alert, + Button, DataList, DataListCheck, DataListItem, DataListItemRow, DataListCell, DataListItemCells, + Form, Grid, GridItem, + Modal, + ModalBody, + ModalHeader, + ModalVariant, } from '@patternfly/react-core'; import { useTranslation } from 'react-i18next'; import { ColumnLayout, ManagedColumn } from '@console/dynamic-plugin-sdk'; @@ -23,8 +29,8 @@ import { withUserPreferenceCompatibility, } from '@console/shared/src/hoc/withUserPreferenceCompatibility'; import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; +import { ModalFooterWithAlerts } from '@console/shared/src/components/modals/ModalFooterWithAlerts'; import type { ModalComponentProps } from '../factory'; -import { ModalTitle, ModalBody, ModalSubmitFooter, ModalWrapper } from '../factory'; export const MAX_VIEW_COLS = 9; @@ -117,83 +123,90 @@ export const ColumnManagementModal: FC< }; return ( -
- {t('public~Manage columns')} + <> + - {!noLimit ? ( - <> -
+ + {!noLimit ? ( + <>

{t('public~Selected columns will appear in the table.')}

-
-
- {!columnLayout?.showNamespaceOverride && } + {columnLayout?.showNamespaceOverride && } -
- - ) : ( - !columnLayout?.showNamespaceOverride && ( -
- -
- ) - )} - - - - - {defaultColumns.map((defaultColumn) => ( - - ))} - - - - - - {additionalColumns.map((additionalColumn) => ( - - ))} - - - + + ) : ( + columnLayout?.showNamespaceOverride && + )} + + + + + {defaultColumns.map((defaultColumn) => ( + + ))} + + + + + + {additionalColumns.map((additionalColumn) => ( + + ))} + + + +
- - + + + + + + ); }; @@ -210,13 +223,18 @@ const ColumnManagementModalWithSettings = withUserPreferenceCompatibility< export const ColumnManagementModalOverlay: OverlayComponent = ( props, ) => ( - + - + ); ColumnManagementModal.displayName = 'ColumnManagementModal'; diff --git a/frontend/public/components/modals/configure-count-modal.tsx b/frontend/public/components/modals/configure-count-modal.tsx index ab5250972c9..ed5846ed9d6 100644 --- a/frontend/public/components/modals/configure-count-modal.tsx +++ b/frontend/public/components/modals/configure-count-modal.tsx @@ -74,7 +74,12 @@ export const ConfigureCountModal: OverlayComponent = ( }; return ( - + { const { pvc, close, cancel } = props; @@ -52,30 +55,43 @@ const DeletePVCModal = (props: DeletePVCModalProps) => { ); return ( -
- - {' '} - {t('public~Delete PersistentVolumeClaim')} - + <> + - - {alertComponents} - - - Are you sure you want to delete{' '} - {{ pvcName }} PersistentVolumeClaim? - - - + + + {alertComponents} + + + Are you sure you want to delete{' '} + {{ pvcName }} PersistentVolumeClaim? + + + + - - + + + + + ); }; @@ -85,8 +101,13 @@ export type DeletePVCModalProps = { export const DeletePVCModalOverlay: OverlayComponent = (props) => { return ( - + - +
); }; diff --git a/frontend/public/components/modals/error-modal.tsx b/frontend/public/components/modals/error-modal.tsx index fb10bf1d49a..1f5a2786af0 100644 --- a/frontend/public/components/modals/error-modal.tsx +++ b/frontend/public/components/modals/error-modal.tsx @@ -1,54 +1,36 @@ import { useCallback } from 'react'; import { - ActionGroup, Button, ButtonVariant, Modal, + ModalBody, + ModalFooter, ModalHeader, ModalVariant, - ModalBody as PfModalBody, - ModalFooter as PfModalFooter, } from '@patternfly/react-core'; import { useTranslation } from 'react-i18next'; import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; -import { ModalTitle, ModalBody, ModalFooter, ModalComponentProps } from '../factory/modal'; -import { YellowExclamationTriangleIcon } from '@console/shared/src/components/status/icons'; - -export const ModalErrorContent = (props: ErrorModalProps) => { - const { t } = useTranslation(); - const { error, title, cancel } = props; - const titleText = title || t('public~Error'); - return ( -
- - {titleText} - - {error} - - - - - -
- ); -}; +import { ModalComponentProps } from '../factory/modal'; export const ErrorModal: OverlayComponent = (props) => { const { t } = useTranslation(); const { error, title } = props; const titleText = title || t('public~Error'); return ( - + - {error} - + {error} + - + ); }; diff --git a/frontend/public/components/modals/index.ts b/frontend/public/components/modals/index.ts index 9317fcc388a..7dc83169750 100644 --- a/frontend/public/components/modals/index.ts +++ b/frontend/public/components/modals/index.ts @@ -87,6 +87,13 @@ export const LazyClusterUpdateModalOverlay = lazy(() => })), ); +// Lazy-loaded OverlayComponent for Taints Modal +export const LazyTaintsModalOverlay = lazy(() => + import('./taints-modal' /* webpackChunkName: "taints-modal" */).then((m) => ({ + default: m.TaintsModalOverlay, + })), +); + // Lazy-loaded OverlayComponent for Tolerations Modal export const LazyTolerationsModalOverlay = lazy(() => import('./tolerations-modal' /* webpackChunkName: "tolerations-modal" */).then((m) => ({ diff --git a/frontend/public/components/modals/rollback-modal.tsx b/frontend/public/components/modals/rollback-modal.tsx index c62163f8d63..aa8a418943a 100644 --- a/frontend/public/components/modals/rollback-modal.tsx +++ b/frontend/public/components/modals/rollback-modal.tsx @@ -2,15 +2,24 @@ import type { FC, FormEvent } from 'react'; import { useState, useEffect } from 'react'; import * as _ from 'lodash'; import { Trans, useTranslation } from 'react-i18next'; -import { Alert, Checkbox } from '@patternfly/react-core'; +import { + Alert, + Button, + Checkbox, + Form, + Modal, + ModalBody, + ModalHeader, + ModalVariant, +} from '@patternfly/react-core'; import type { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; import { getDeploymentConfigVersion, getOwnerNameByKind, } from '@console/shared/src/utils/resource-utils'; import { usePromiseHandler } from '@console/shared/src/hooks/promise-handler'; -import { ModalTitle, ModalBody, ModalSubmitFooter, ModalWrapper } from '../factory/modal'; import type { ModalComponentProps } from '../factory/modal'; +import { ModalFooterWithAlerts } from '@console/shared/src/components/modals/ModalFooterWithAlerts'; import { LoadingInline } from '../utils/status-box'; import { DeploymentConfigModel, DeploymentModel, ReplicationControllerModel } from '../../models'; import type { K8sResourceKind } from '../../module/k8s'; @@ -110,9 +119,11 @@ const BaseRollbackModal: FC = (props) => { { op: 'replace', path: '/metadata/annotations', value: annotations }, ]; - handlePromise(k8sPatch(DeploymentModel, deployment, patch)).then(() => { - props.close(); - }); + handlePromise(k8sPatch(DeploymentModel, deployment, patch)) + .then(() => { + props.close(); + }) + .catch(() => {}); }; const submit = (e: FormEvent) => { @@ -187,41 +198,61 @@ const BaseRollbackModal: FC = (props) => { }; return ( -
- {t('public~Rollback')} + <> + - {loaded ? ( - !loadError && !deploymentError ? ( - renderRollbackBody() + + {loaded ? ( + !loadError && !deploymentError ? ( + renderRollbackBody() + ) : ( + +
{loadError?.message || deploymentError}
+
+ ) ) : ( - -
{loadError?.message || deploymentError}
-
- ) - ) : ( - - )} + + )} +
- - + + + + + ); }; export const RollbackModalOverlay: OverlayComponent = (props) => ( - + - +
); export type RollbackModalProps = { diff --git a/frontend/public/components/modals/taints-modal.tsx b/frontend/public/components/modals/taints-modal.tsx index 2b4d2f81143..ab2615f144e 100644 --- a/frontend/public/components/modals/taints-modal.tsx +++ b/frontend/public/components/modals/taints-modal.tsx @@ -1,7 +1,16 @@ import * as _ from 'lodash'; import type { FormEvent } from 'react'; import { useState } from 'react'; -import { Button, Tooltip } from '@patternfly/react-core'; +import { + Button, + Form, + Modal, + ModalBody, + ModalHeader, + ModalVariant, + TextInput, + Tooltip, +} from '@patternfly/react-core'; import { Table, Thead, Tr, Th, Td, Tbody } from '@patternfly/react-table'; import { MinusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/minus-circle-icon'; import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon'; @@ -10,15 +19,10 @@ import { useTranslation } from 'react-i18next'; import { ConsoleSelect } from '@console/internal/components/utils/console-select'; import { EmptyBox } from '../utils/status-box'; import { K8sKind, NodeKind, k8sPatch, Taint } from '../../module/k8s'; -import { - ModalBody, - ModalComponentProps, - ModalSubmitFooter, - ModalTitle, - ModalWrapper, -} from '../factory'; +import { ModalComponentProps } from '../factory'; import { usePromiseHandler } from '@console/shared/src/hooks/promise-handler'; import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; +import { ModalFooterWithAlerts } from '@console/shared/src/components/modals/ModalFooterWithAlerts'; const TaintsModal = (props: TaintsModalProps) => { const [taints, setTaints] = useState(props.resource.spec.taints || []); @@ -43,7 +47,8 @@ const TaintsModal = (props: TaintsModalProps) => { }; const change = (e, i: number, field: string) => { - const newValue = e.target ? e.target.value : e; + // Handle both native events (from input) and values (from ConsoleSelect) + const newValue = typeof e === 'string' ? e : e.target?.value ?? e; setTaints((prevTaints) => { const clonedTaints = _.cloneDeep(prevTaints); clonedTaints[i][field] = newValue; @@ -72,91 +77,110 @@ const TaintsModal = (props: TaintsModalProps) => { }; return ( -
- {t('Edit taints')} + <> + - {_.isEmpty(taints) ? ( - - ) : ( - - - - - - - - - - - {_.map(taints, (c, i: number) => ( - - + + + + ))} + +
{t('Key')}{t('Value')}{t('Effect')}
- - + {_.isEmpty(taints) ? ( + + ) : ( + + + + + + + + + + + {_.map(taints, (c, i: number) => ( + + - - - + - - ))} - -
{t('Key')}{t('Value')}{t('Effect')}
+ change(e, i, 'key')} - required + onChange={(_event, value) => change(value, i, 'key')} + isRequired + aria-label={t('Key')} /> - - - - change(e, i, 'value')} /> - - - change(e, i, 'effect')} - selectedKey={c.effect} - title={effects[c.effect]} - alwaysShowTitle - /> - - - + change(value, i, 'value')} + aria-label={t('Value')} /> - -
- )} +
+ change(e, i, 'effect')} + selectedKey={c.effect} + title={effects[c.effect]} + alwaysShowTitle + /> + + +
+ )} + + +
+ - - - + + + ); }; @@ -167,9 +191,14 @@ export type TaintsModalProps = { const TaintsModalOverlay: OverlayComponent = (props) => { return ( - + - +
); }; diff --git a/frontend/public/components/modals/tolerations-modal.tsx b/frontend/public/components/modals/tolerations-modal.tsx index 05ea08c13c4..7843c4a3092 100644 --- a/frontend/public/components/modals/tolerations-modal.tsx +++ b/frontend/public/components/modals/tolerations-modal.tsx @@ -2,24 +2,26 @@ import * as _ from 'lodash'; import type { FormEvent } from 'react'; import { useState } from 'react'; import { css } from '@patternfly/react-styles'; -import { Button, Tooltip } from '@patternfly/react-core'; +import { + Button, + Form, + Modal, + ModalBody, + ModalHeader, + ModalVariant, + Tooltip, +} from '@patternfly/react-core'; import { Table, Thead, Tr, Th, Td, Tbody } from '@patternfly/react-table'; import { MinusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/minus-circle-icon'; import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon'; import { useTranslation } from 'react-i18next'; - import { ConsoleSelect } from '@console/internal/components/utils/console-select'; import { EmptyBox } from '../utils/status-box'; import { K8sKind, k8sPatch, Toleration, TolerationOperator } from '../../module/k8s'; import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; -import { - ModalWrapper, - ModalBody, - ModalComponentProps, - ModalSubmitFooter, - ModalTitle, -} from '../factory'; +import type { ModalComponentProps } from '../factory'; import { usePromiseHandler } from '@console/shared/src/hooks/promise-handler'; +import { ModalFooterWithAlerts } from '@console/shared/src/components/modals/ModalFooterWithAlerts'; const TolerationsModal = (props: TolerationsModalProps) => { const getTolerationsFromResource = (): Toleration[] => { @@ -109,141 +111,161 @@ const TolerationsModal = (props: TolerationsModalProps) => { }; return ( -
- {t('public~Edit tolerations')} + <> + - {_.isEmpty(tolerations) ? ( - - ) : ( - - - - - - - - - + + {_.isEmpty(tolerations) ? ( + + ) : ( +
{t('public~Key')}{t('public~Operator')}{t('public~Value')}{t('public~Effect')}
+ + + + + + + + - - {_.map(tolerations, (toleration, i) => { - const { key, operator, value, effect = '' } = toleration; - const keyReadOnly = !isEditable(toleration); - const valueReadOnly = !isEditable(toleration) || operator === 'Exists'; - return ( - - - + {_.map(tolerations, (toleration, i) => { + const { key, operator, value, effect = '' } = toleration; + const keyReadOnly = !isEditable(toleration); + const valueReadOnly = !isEditable(toleration) || operator === 'Exists'; + return ( + + - - + + - + - - ); - })} - -
{t('public~Key')}{t('public~Operator')}{t('public~Value')}{t('public~Effect')}
- - change(e, i, 'key')} - readOnly={keyReadOnly} - /> - - - {isEditable(toleration) ? ( - opChange(op, i)} - selectedKey={operator} - title={operators[operator]} - alwaysShowTitle - /> - ) : ( - - +
+ + change(e, i, 'key')} + readOnly={keyReadOnly} + /> - )} - - - change(e, i, 'value')} - readOnly={valueReadOnly} - /> - - - {isEditable(toleration) ? ( - change(e, i, 'effect')} - selectedKey={effect} - title={effects[effect]} - alwaysShowTitle - /> - ) : ( - - + + {isEditable(toleration) ? ( + opChange(op, i)} + selectedKey={operator} + title={operators[operator]} + alwaysShowTitle + /> + ) : ( + + + + )} + + + change(e, i, 'value')} + readOnly={valueReadOnly} + /> - )} - - {isEditable(toleration) && ( - - + {isEditable(toleration) ? ( + change(e, i, 'effect')} + selectedKey={effect} + title={effects[effect]} + alwaysShowTitle /> - - )} -
- )} + ) : ( + + + + )} + + + {isEditable(toleration) && ( + + + +
+ - - - + + + ); }; export const TolerationsModalOverlay: OverlayComponent = (props) => { return ( - + - + ); };