diff --git a/frontend/packages/console-app/src/actions/creators/hpa-factory.ts b/frontend/packages/console-app/src/actions/creators/hpa-factory.ts index 7ad609ca5a6..16eaf393e42 100644 --- a/frontend/packages/console-app/src/actions/creators/hpa-factory.ts +++ b/frontend/packages/console-app/src/actions/creators/hpa-factory.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import i18next from 'i18next'; import type { Action } from '@console/dynamic-plugin-sdk'; +import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import { HorizontalPodAutoscalerModel } from '@console/internal/models'; import type { @@ -12,84 +13,23 @@ import type { import { referenceForModel } from '@console/internal/module/k8s'; import type { ClusterServiceVersionKind } from '@console/operator-lifecycle-manager'; import { ClusterServiceVersionModel } from '@console/operator-lifecycle-manager'; -import deleteHPAModal from '@console/shared/src/components/hpa/DeleteHPAModal'; +import { LazyDeleteHPAModalOverlay } from '@console/shared/src/components/hpa'; import { isHelmResource } from '@console/shared/src/utils/helm-utils'; import { doesHpaMatch } from '@console/shared/src/utils/hpa-utils'; import { isOperatorBackedService } from '@console/shared/src/utils/operator-utils'; -import type { ResourceActionFactory } from './types'; const hpaRoute = ( { metadata: { name = '', namespace = '' } = {} }: K8sResourceCommon, kind: K8sKind, ) => `/workload-hpa/ns/${namespace}/${referenceForModel(kind)}/${name}`; -export const HpaActionFactory: ResourceActionFactory = { - AddHorizontalPodAutoScaler: (kind: K8sKind, obj: K8sResourceKind) => ({ - id: 'add-hpa', - label: i18next.t('console-app~Add HorizontalPodAutoscaler'), - cta: { href: hpaRoute(obj, kind) }, - insertBefore: 'add-pdb', - accessReview: { - group: HorizontalPodAutoscalerModel.apiGroup, - resource: HorizontalPodAutoscalerModel.plural, - namespace: obj.metadata?.namespace, - verb: 'create', - }, - }), - EditHorizontalPodAutoScaler: (kind: K8sKind, obj: K8sResourceCommon) => ({ - id: 'edit-hpa', - label: i18next.t('console-app~Edit HorizontalPodAutoscaler'), - cta: { href: hpaRoute(obj, kind) }, - insertBefore: 'add-pdb', - accessReview: { - group: HorizontalPodAutoscalerModel.apiGroup, - resource: HorizontalPodAutoscalerModel.plural, - namespace: obj.metadata?.namespace, - verb: 'update', - }, - }), - DeleteHorizontalPodAutoScaler: ( - kind: K8sKind, - obj: K8sResourceCommon, - relatedHPA: HorizontalPodAutoscalerKind, - ) => ({ - id: 'delete-hpa', - label: i18next.t('console-app~Remove HorizontalPodAutoscaler'), - insertBefore: 'delete-pdb', - cta: () => { - deleteHPAModal({ - workload: obj, - hpa: relatedHPA, - }); - }, - accessReview: { - group: HorizontalPodAutoscalerModel.apiGroup, - resource: HorizontalPodAutoscalerModel.plural, - namespace: obj.metadata?.namespace, - verb: 'delete', - }, - }), -}; - -export const getHpaActions = ( - kind: K8sKind, - obj: K8sResourceKind, - relatedHPAs: K8sResourceKind[], -): Action[] => { - if (relatedHPAs.length === 0) return [HpaActionFactory.AddHorizontalPodAutoScaler(kind, obj)]; - - return [ - HpaActionFactory.EditHorizontalPodAutoScaler(kind, obj), - HpaActionFactory.DeleteHorizontalPodAutoScaler(kind, obj, relatedHPAs[0]), - ]; -}; - type DeploymentActionExtraResources = { hpas: HorizontalPodAutoscalerKind[]; csvs: ClusterServiceVersionKind[]; }; export const useHPAActions = (kindObj: K8sKind, resource: K8sResourceKind) => { + const launchModal = useOverlay(); const namespace = resource?.metadata?.namespace; const watchedResources = useMemo( @@ -121,9 +61,63 @@ export const useHPAActions = (kindObj: K8sKind, resource: K8sResourceKind) => { [extraResources.csvs.data, resource], ); - const result = useMemo<[Action[], HorizontalPodAutoscalerKind[]]>(() => { - return [supportsHPA ? getHpaActions(kindObj, resource, relatedHPAs) : [], relatedHPAs]; + const actions = useMemo(() => { + if (!supportsHPA) return []; + + if (relatedHPAs.length === 0) { + return [ + { + id: 'add-hpa', + label: i18next.t('console-app~Add HorizontalPodAutoscaler'), + cta: { href: hpaRoute(resource, kindObj) }, + insertBefore: 'add-pdb', + accessReview: { + group: HorizontalPodAutoscalerModel.apiGroup, + resource: HorizontalPodAutoscalerModel.plural, + namespace: resource.metadata?.namespace, + verb: 'create', + }, + }, + ]; + } + return [ + { + id: 'edit-hpa', + label: i18next.t('console-app~Edit HorizontalPodAutoscaler'), + cta: { href: hpaRoute(resource, kindObj) }, + insertBefore: 'add-pdb', + accessReview: { + group: HorizontalPodAutoscalerModel.apiGroup, + resource: HorizontalPodAutoscalerModel.plural, + namespace: resource.metadata?.namespace, + verb: 'update', + }, + }, + { + id: 'delete-hpa', + label: i18next.t('console-app~Remove HorizontalPodAutoscaler'), + insertBefore: 'delete-pdb', + cta: () => { + launchModal(LazyDeleteHPAModalOverlay, { + workload: resource, + hpa: relatedHPAs[0], + }); + }, + accessReview: { + group: HorizontalPodAutoscalerModel.apiGroup, + resource: HorizontalPodAutoscalerModel.plural, + namespace: resource.metadata?.namespace, + verb: 'delete', + }, + }, + ]; + // Missing launchModal dependency causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps }, [kindObj, relatedHPAs, resource, supportsHPA]); + const result = useMemo<[Action[], HorizontalPodAutoscalerKind[]]>(() => { + return [actions, relatedHPAs]; + }, [actions, relatedHPAs]); + return result; }; diff --git a/frontend/packages/console-app/src/components/console-operator/ConsoleOperatorConfig.tsx b/frontend/packages/console-app/src/components/console-operator/ConsoleOperatorConfig.tsx index 296805d7ce8..e537b8c80c4 100644 --- a/frontend/packages/console-app/src/components/console-operator/ConsoleOperatorConfig.tsx +++ b/frontend/packages/console-app/src/components/console-operator/ConsoleOperatorConfig.tsx @@ -11,6 +11,7 @@ import { Link } from 'react-router-dom'; import { useLocation } from 'react-router-dom-v5-compat'; import type { WatchK8sResource } from '@console/dynamic-plugin-sdk'; import { useAccessReview } from '@console/dynamic-plugin-sdk'; +import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; import { getGroupVersionKindForModel, getReferenceForModel, @@ -35,7 +36,7 @@ import { referenceForModel } from '@console/internal/module/k8s'; import type { RootState } from '@console/internal/redux'; import { usePluginInfo } from '@console/plugin-sdk/src/api/usePluginInfo'; import PaneBody from '@console/shared/src/components/layout/PaneBody'; -import { consolePluginModal } from '@console/shared/src/components/modals/ConsolePluginModal'; +import { LazyConsolePluginModalOverlay } from '@console/shared/src/components/modals'; import { GreenCheckCircleIcon, YellowExclamationTriangleIcon, @@ -102,6 +103,7 @@ export const ConsolePluginEnabledStatus: FC = ( enabled, }) => { const { t } = useTranslation('console-app'); + const launchModal = useOverlay(); const { consoleOperatorConfig, @@ -121,7 +123,7 @@ export const ConsolePluginEnabledStatus: FC = ( type="button" isInline onClick={() => - consolePluginModal({ + launchModal(LazyConsolePluginModalOverlay, { consoleOperatorConfig, pluginName, trusted: false, diff --git a/frontend/packages/console-app/src/components/favorite/FavoriteButton.tsx b/frontend/packages/console-app/src/components/favorite/FavoriteButton.tsx index 534bc81ee31..4a932bbfc00 100644 --- a/frontend/packages/console-app/src/components/favorite/FavoriteButton.tsx +++ b/frontend/packages/console-app/src/components/favorite/FavoriteButton.tsx @@ -9,10 +9,13 @@ import { HelperTextItem, TextInput, Tooltip, + Modal, + ModalVariant, + ModalHeader, + ModalBody, + ModalFooter, } from '@patternfly/react-core'; -import { ModalVariant } from '@patternfly/react-core/deprecated'; import { useTranslation } from 'react-i18next'; -import Modal from '@console/shared/src/components/modal/Modal'; import { useTelemetry } from '@console/shared/src/hooks/useTelemetry'; import { useUserSettingsCompatibility } from '@console/shared/src/hooks/useUserSettingsCompatibility'; import { FAVORITES_CONFIG_MAP_KEY, FAVORITES_LOCAL_STORAGE_KEY } from '../../consts'; @@ -78,7 +81,8 @@ export const FavoriteButton = ({ defaultName }: FavoriteButtonProps) => { setIsModalOpen(false); }; - const handleConfirmStar = () => { + const handleConfirmStar = (e?: React.FormEvent) => { + e?.preventDefault(); const trimmedName = name.trim(); if (!trimmedName) { setError(t('Name is required.')); @@ -151,46 +155,44 @@ export const FavoriteButton = ({ defaultName }: FavoriteButtonProps) => { {isModalOpen && ( - + + +
+ + handleNameChange(v)} + value={name || ''} + autoFocus + required + /> + {error && ( + + + {error} + + + )} + +
+
+ , + , - ]} - variant={ModalVariant.small} - > -
- - handleNameChange(v)} - value={name || ''} - autoFocus - required - /> - {error && ( - - - {error} - - - )} - -
+ +
)} diff --git a/frontend/packages/console-app/src/components/modals/add-group-users-modal.tsx b/frontend/packages/console-app/src/components/modals/add-group-users-modal.tsx index da15972b46c..8e045f1825a 100644 --- a/frontend/packages/console-app/src/components/modals/add-group-users-modal.tsx +++ b/frontend/packages/console-app/src/components/modals/add-group-users-modal.tsx @@ -91,9 +91,6 @@ const AddGroupUsersModal: OverlayComponent = ({ group, )} - + ); diff --git a/frontend/packages/console-app/src/components/tour/TourStepComponent.tsx b/frontend/packages/console-app/src/components/tour/TourStepComponent.tsx index 4d629e3e479..03acc787c28 100644 --- a/frontend/packages/console-app/src/components/tour/TourStepComponent.tsx +++ b/frontend/packages/console-app/src/components/tour/TourStepComponent.tsx @@ -3,6 +3,7 @@ import { useContext } from 'react'; import { Grid, GridItem, + Modal, ModalBody, ModalFooter, ModalHeader, @@ -10,7 +11,6 @@ import { } from '@patternfly/react-core'; import { useTranslation } from 'react-i18next'; import { ThemeContext } from '@console/internal/components/ThemeProvider'; -import Modal from '@console/shared/src/components/modal/Modal'; import type { PopoverPlacement } from '@console/shared/src/components/popover/const'; import Popover from '@console/shared/src/components/popover/Popover'; import Spotlight from '@console/shared/src/components/spotlight/Spotlight'; @@ -108,7 +108,6 @@ const TourStepComponent: FC = ({ id="guided-tour-modal" data-test="guided-tour-modal" aria-label={t('console-app~guided tour {{step, number}}', { step })} - isFullScreen > diff --git a/frontend/packages/console-shared/locales/en/console-shared.json b/frontend/packages/console-shared/locales/en/console-shared.json index 98dda309822..d7e135f9488 100644 --- a/frontend/packages/console-shared/locales/en/console-shared.json +++ b/frontend/packages/console-shared/locales/en/console-shared.json @@ -174,7 +174,6 @@ "Container {{containersName}} does not have health checks to ensure your Application is running correctly.": "Container {{containersName}} does not have health checks to ensure your Application is running correctly.", "Health checks": "Health checks", "Add health checks": "Add health checks", - "Unknown error removing {{hpaLabel}} {{hpaName}}.": "Unknown error removing {{hpaLabel}} {{hpaName}}.", "Remove {{label}}?": "Remove {{label}}?", "Are you sure you want to remove the {{hpaLabel}}": "Are you sure you want to remove the {{hpaLabel}}", "from": "from", @@ -205,7 +204,7 @@ "Description": "Description", "Delete": "Delete", "This action cannot be undone. All associated Deployments, Routes, Builds, Pipelines, Storage/PVCs, Secrets, and ConfigMaps will be deleted.": "This action cannot be undone. All associated Deployments, Routes, Builds, Pipelines, Storage/PVCs, Secrets, and ConfigMaps will be deleted.", - "Confirm deletion by typing <1>{{resourceName}} below:": "Confirm deletion by typing <1>{{resourceName}} below:", + "Confirm deletion by typing <2>{{resourceName}} below:": "Confirm deletion by typing <2>{{resourceName}} below:", "Description:": "Description:", "Component trace:": "Component trace:", "Stack trace:": "Stack trace:", diff --git a/frontend/packages/console-shared/src/components/actions/menu/ActionMenuItem.tsx b/frontend/packages/console-shared/src/components/actions/menu/ActionMenuItem.tsx index 185ab86cae1..e1522ce88be 100644 --- a/frontend/packages/console-shared/src/components/actions/menu/ActionMenuItem.tsx +++ b/frontend/packages/console-shared/src/components/actions/menu/ActionMenuItem.tsx @@ -5,10 +5,10 @@ import { KeyTypes, MenuItem, Tooltip } from '@patternfly/react-core'; import { css } from '@patternfly/react-styles'; import * as _ from 'lodash'; import { connect } from 'react-redux'; +import { useNavigate } from 'react-router-dom-v5-compat'; import type { Action, ImpersonateKind } from '@console/dynamic-plugin-sdk'; import { impersonateStateToProps } from '@console/dynamic-plugin-sdk'; import { useAccessReview } from '@console/internal/components/utils/rbac'; -import { history } from '@console/internal/components/utils/router'; export type ActionMenuItemProps = { action: Action; @@ -26,6 +26,7 @@ const ActionItem: FC = ({ isAllowed, component, }) => { + const navigate = useNavigate(); const { label, icon, disabled, cta } = action; const { href, external } = cta as { href: string; external?: boolean }; const isDisabled = !isAllowed || disabled; @@ -38,13 +39,13 @@ const ActionItem: FC = ({ cta(); } else if (_.isObject(cta)) { if (!cta.external) { - history.push(cta.href); + navigate(cta.href); } } onClick && onClick(); event.stopPropagation(); }, - [cta, onClick], + [cta, onClick, navigate], ); const handleKeyDown = (event) => { diff --git a/frontend/packages/console-shared/src/components/catalog/CatalogTile.tsx b/frontend/packages/console-shared/src/components/catalog/CatalogTile.tsx index c6b88b9100e..774fe624eba 100644 --- a/frontend/packages/console-shared/src/components/catalog/CatalogTile.tsx +++ b/frontend/packages/console-shared/src/components/catalog/CatalogTile.tsx @@ -7,8 +7,8 @@ import { import { Badge } from '@patternfly/react-core'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import type { CatalogItem } from '@console/dynamic-plugin-sdk/src/extensions'; -import { history } from '@console/internal/components/utils/router'; import { isModifiedEvent } from '../../utils'; import CatalogBadges from './CatalogBadges'; import { getIconProps } from './utils/catalog-utils'; @@ -25,6 +25,7 @@ type CatalogTileProps = { const CatalogTile: FC = ({ item, catalogTypes, onClick, href }) => { const { t } = useTranslation(); + const navigate = useNavigate(); const { uid, name, title, provider, description, type, typeLabel, badges } = item; const vendor = provider ? t('console-shared~Provided by {{provider}}', { provider }) : null; const catalogType = _.find(catalogTypes, ['value', type]); @@ -46,7 +47,7 @@ const CatalogTile: FC = ({ item, catalogTypes, onClick, href } if (onClick) { onClick(item); } else if (href) { - history.push(href); + navigate(href); } }} href={href} diff --git a/frontend/packages/console-shared/src/components/catalog/catalog-view/CatalogView.tsx b/frontend/packages/console-shared/src/components/catalog/catalog-view/CatalogView.tsx index a3e387bca8c..939a3065e9a 100644 --- a/frontend/packages/console-shared/src/components/catalog/catalog-view/CatalogView.tsx +++ b/frontend/packages/console-shared/src/components/catalog/catalog-view/CatalogView.tsx @@ -2,6 +2,7 @@ import type { ReactNode, FC } from 'react'; import { useMemo, useState, useRef, useCallback, useEffect } from 'react'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import type { CatalogCategory } from '@console/dynamic-plugin-sdk/src'; import type { CatalogItem } from '@console/dynamic-plugin-sdk/src/extensions'; import { isModalOpen } from '@console/internal/components/modals'; @@ -76,6 +77,7 @@ const CatalogView: FC = ({ sortFilterGroups, }) => { const { t } = useTranslation(); + const navigate = useNavigate(); const queryParams = useQueryParams(); const activeCategoryId = queryParams.get(CatalogQueryParams.CATEGORY) ?? ALL_CATEGORY; const activeSearchKeyword = queryParams.get(CatalogQueryParams.KEYWORD) ?? ''; @@ -110,37 +112,49 @@ const CatalogView: FC = ({ const clearFilters = useCallback(() => { const params = new URLSearchParams(); catalogType && items.length > 0 && params.set('catalogType', catalogType); - setURLParams(params); + setURLParams(params, navigate); // Don't take focus if a modal was opened while the page was loading. if (!isModalOpen()) { catalogToolbarRef.current && catalogToolbarRef.current.focus({ preventScroll: true }); } - }, [catalogType, items.length]); + }, [catalogType, items.length, navigate]); - const handleCategoryChange = (categoryId: string) => { - updateURLParams(CatalogQueryParams.CATEGORY, categoryId); - }; + const handleCategoryChange = useCallback( + (categoryId: string) => { + updateURLParams(CatalogQueryParams.CATEGORY, categoryId, navigate); + }, + [navigate], + ); const handleFilterChange = useCallback( (filterType, id, value) => { const updatedFilters = _.set(activeFilters, [filterType, id, 'active'], value); - updateURLParams(filterType, getFilterSearchParam(updatedFilters[filterType])); + updateURLParams(filterType, getFilterSearchParam(updatedFilters[filterType]), navigate); }, - [activeFilters], + [activeFilters, navigate], ); - const handleSearchKeywordChange = useCallback((searchKeyword) => { - updateURLParams(CatalogQueryParams.KEYWORD, searchKeyword); - }, []); + const handleSearchKeywordChange = useCallback( + (searchKeyword) => { + updateURLParams(CatalogQueryParams.KEYWORD, searchKeyword, navigate); + }, + [navigate], + ); - const handleGroupingChange = useCallback((grouping) => { - updateURLParams(CatalogQueryParams.GROUPING, grouping); - }, []); + const handleGroupingChange = useCallback( + (grouping) => { + updateURLParams(CatalogQueryParams.GROUPING, grouping, navigate); + }, + [navigate], + ); - const handleSortOrderChange = useCallback((order) => { - updateURLParams(CatalogQueryParams.SORT_ORDER, order); - }, []); + const handleSortOrderChange = useCallback( + (order) => { + updateURLParams(CatalogQueryParams.SORT_ORDER, order, navigate); + }, + [navigate], + ); const handleShowAllToggle = useCallback((groupName) => { setFilterGroupsShowAll((showAll) => { diff --git a/frontend/packages/console-shared/src/components/modal/Modal.scss b/frontend/packages/console-shared/src/components/catalog/details/CatalogDetailsModal.scss similarity index 100% rename from frontend/packages/console-shared/src/components/modal/Modal.scss rename to frontend/packages/console-shared/src/components/catalog/details/CatalogDetailsModal.scss diff --git a/frontend/packages/console-shared/src/components/catalog/details/CatalogDetailsModal.tsx b/frontend/packages/console-shared/src/components/catalog/details/CatalogDetailsModal.tsx index 5377e70f759..07983dc679f 100644 --- a/frontend/packages/console-shared/src/components/catalog/details/CatalogDetailsModal.tsx +++ b/frontend/packages/console-shared/src/components/catalog/details/CatalogDetailsModal.tsx @@ -1,14 +1,23 @@ import type { FC } from 'react'; import { CatalogItemHeader } from '@patternfly/react-catalog-view-extension'; -import { Split, SplitItem, Divider, Stack, StackItem } from '@patternfly/react-core'; +import { + Split, + SplitItem, + Divider, + Stack, + StackItem, + Modal, + ModalBody, + ModalHeader, +} from '@patternfly/react-core'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom-v5-compat'; import type { CatalogItem } from '@console/dynamic-plugin-sdk/src/extensions'; -import { Modal } from '../../modal'; import CatalogBadges from '../CatalogBadges'; import { useCtaLink } from '../hooks/useCtaLink'; import { getIconProps } from '../utils/catalog-utils'; import CatalogDetailsPanel from './CatalogDetailsPanel'; +import './CatalogDetailsModal.scss'; type CatalogDetailsModalProps = { item: CatalogItem; @@ -42,43 +51,45 @@ const CatalogDetailsModal: FC = ({ item, onClose }) => return ( - - - - - {to && ( -
- - {label} - -
- )} -
- - - {badges?.length > 0 ? : undefined} - -
-
- - - - - - -
+ {modalHeader} + + + + + + {to && ( +
+ + {label} + +
+ )} +
+ + + {badges?.length > 0 ? : undefined} + +
+
+ + + + + + +
+
); }; diff --git a/frontend/packages/console-shared/src/components/catalog/utils/catalog-utils.tsx b/frontend/packages/console-shared/src/components/catalog/utils/catalog-utils.tsx index f180eb1cb3f..a48c7e9b20a 100644 --- a/frontend/packages/console-shared/src/components/catalog/utils/catalog-utils.tsx +++ b/frontend/packages/console-shared/src/components/catalog/utils/catalog-utils.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react'; import * as _ from 'lodash'; +import type { NavigateFunction } from 'react-router-dom-v5-compat'; import type { CatalogItemType } from '@console/dynamic-plugin-sdk'; import { isCatalogItemType } from '@console/dynamic-plugin-sdk'; import type { @@ -8,7 +9,6 @@ import type { CatalogItemMetadataProviderFunction, } from '@console/dynamic-plugin-sdk/src/extensions'; import { normalizeIconClass } from '@console/internal/components/catalog/catalog-item-icon'; -import { history } from '@console/internal/components/utils/router'; import catalogImg from '@console/internal/imgs/logos/catalog-icon.svg'; import { useExtensions } from '@console/plugin-sdk/src/api/useExtensions'; import type { CatalogType, CatalogTypeCounts } from './types'; @@ -299,14 +299,18 @@ export const getIconProps = (item: CatalogItem) => { return { iconImg: catalogImg, iconClass: null }; }; -export const setURLParams = (params: URLSearchParams) => { +export const setURLParams = (params: URLSearchParams, navigate: NavigateFunction) => { const url = new URL(window.location.href); const searchParams = `?${params.toString()}${url.hash}`; - history.replace(`${url.pathname}${searchParams}`); + navigate(`${url.pathname}${searchParams}`, { replace: true }); }; -export const updateURLParams = (paramName: string, value: string | string[]) => { +export const updateURLParams = ( + paramName: string, + value: string | string[], + navigate: NavigateFunction, +) => { const params = new URLSearchParams(window.location.search); if (value) { @@ -314,7 +318,7 @@ export const updateURLParams = (paramName: string, value: string | string[]) => } else { params.delete(paramName); } - setURLParams(params); + setURLParams(params, navigate); }; export const getURLWithParams = (paramName: string, value: string | string[]): string => { diff --git a/frontend/packages/console-shared/src/components/dynamic-form/index.tsx b/frontend/packages/console-shared/src/components/dynamic-form/index.tsx index b28cbd0750d..306d9102211 100644 --- a/frontend/packages/console-shared/src/components/dynamic-form/index.tsx +++ b/frontend/packages/console-shared/src/components/dynamic-form/index.tsx @@ -1,11 +1,12 @@ import type { FC } from 'react'; +import { useCallback } from 'react'; import { Accordion, ActionGroup, Button, Alert } from '@patternfly/react-core'; import type { FormProps } from '@rjsf/core'; import Form from '@rjsf/core'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import type { ErrorBoundaryFallbackProps } from '@console/dynamic-plugin-sdk'; -import { history } from '@console/internal/components/utils/router'; import { ErrorBoundary } from '@console/shared/src/components/error'; import { K8S_UI_SCHEMA } from './const'; import defaultFields from './fields'; @@ -42,6 +43,8 @@ export const DynamicForm: FC = ({ ...restProps }) => { const { t } = useTranslation(); + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const schemaErrors = getSchemaErrors(schema); // IF the top level schema is unsupported, don't render a form at all. if (schemaErrors.length) { @@ -112,7 +115,7 @@ export const DynamicForm: FC = ({ - diff --git a/frontend/packages/console-shared/src/components/error/error-boundary.tsx b/frontend/packages/console-shared/src/components/error/error-boundary.tsx index 9ee3be2c5db..3e34e07fefe 100644 --- a/frontend/packages/console-shared/src/components/error/error-boundary.tsx +++ b/frontend/packages/console-shared/src/components/error/error-boundary.tsx @@ -1,13 +1,17 @@ import type { ComponentType, ReactNode, FC } from 'react'; import { Component } from 'react'; +import { useLocation } from 'react-router-dom-v5-compat'; import type { ErrorBoundaryFallbackProps } from '@console/dynamic-plugin-sdk'; -import { history } from '@console/internal/components/utils/router'; type ErrorBoundaryProps = { FallbackComponent?: ComponentType; children?: ReactNode; }; +type ErrorBoundaryInnerProps = ErrorBoundaryProps & { + locationPathname?: string; +}; + /** Needed for tests -- should not be imported by application logic */ export type ErrorBoundaryState = { hasError: boolean; @@ -17,9 +21,7 @@ export type ErrorBoundaryState = { const DefaultFallback: FC = () =>
; -class ErrorBoundary extends Component { - unlisten: () => void = () => {}; - +class ErrorBoundaryInner extends Component { readonly defaultState: ErrorBoundaryState = { hasError: false, error: { @@ -37,15 +39,16 @@ class ErrorBoundary extends Component { this.state = this.defaultState; } - componentDidMount() { - this.unlisten = history.listen(() => { - // reset state to default when location changes + componentDidUpdate(prevProps: ErrorBoundaryInnerProps) { + // Reset error state when location changes + if ( + this.state.hasError && + prevProps.locationPathname && + this.props.locationPathname !== prevProps.locationPathname + ) { + // eslint-disable-next-line react/no-did-update-set-state this.setState(this.defaultState); - }); - } - - componentWillUnmount() { - this.unlisten(); + } } componentDidCatch(error, errorInfo) { @@ -75,4 +78,15 @@ class ErrorBoundary extends Component { } } +// Functional wrapper to handle location changes +const ErrorBoundary: FC = ({ children, FallbackComponent }) => { + const location = useLocation(); + + return ( + + {children} + + ); +}; + export default ErrorBoundary; diff --git a/frontend/packages/console-shared/src/components/hpa/DeleteHPAModal.tsx b/frontend/packages/console-shared/src/components/hpa/DeleteHPAModal.tsx index 309527cc92d..a001db08542 100644 --- a/frontend/packages/console-shared/src/components/hpa/DeleteHPAModal.tsx +++ b/frontend/packages/console-shared/src/components/hpa/DeleteHPAModal.tsx @@ -1,59 +1,47 @@ -import type { FC } from 'react'; import { useState } from 'react'; -import { Form } from '@patternfly/react-core'; -import { ExclamationTriangleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon'; -import { t_global_icon_color_status_warning_default as warningColor } from '@patternfly/react-tokens'; +import type { FC } from 'react'; +import { Button, Form, Modal, ModalBody, ModalHeader, ModalVariant } from '@patternfly/react-core'; import { useTranslation } from 'react-i18next'; +import type { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; import type { ModalComponentProps } from '@console/internal/components/factory/modal'; -import { - createModalLauncher, - ModalBody, - ModalSubmitFooter, - ModalTitle, -} from '@console/internal/components/factory/modal'; import { LoadingInline } from '@console/internal/components/utils/status-box'; import { HorizontalPodAutoscalerModel } from '@console/internal/models'; import type { HorizontalPodAutoscalerKind, K8sResourceCommon } from '@console/internal/module/k8s'; import { k8sKill } from '@console/internal/module/k8s'; +import { usePromiseHandler } from '../../hooks/promise-handler'; +import { ModalFooterWithAlerts } from '../modals/ModalFooterWithAlerts'; +import { YellowExclamationTriangleIcon } from '../status'; type DeleteHPAModalProps = ModalComponentProps & { hpa: HorizontalPodAutoscalerKind; workload: K8sResourceCommon; }; -const DeleteHPAModal: FC = ({ close, hpa, workload }) => { - const [submitError, setSubmitError] = useState(null); - const [isSubmitting, setIsSubmitting] = useState(false); +const DeleteHPAModal: FC = ({ close, cancel, hpa, workload }) => { + const [handlePromise, inProgress, errorMessage] = usePromiseHandler(); const { t } = useTranslation(); const hpaName = hpa.metadata.name; const workloadName = workload.metadata.name; const handleSubmit = (e) => { e.preventDefault(); - setIsSubmitting(true); - k8sKill(HorizontalPodAutoscalerModel, hpa) - .then(() => { - close(); - }) - .catch((error) => { - setSubmitError( - error?.message || - t('console-shared~Unknown error removing {{hpaLabel}} {{hpaName}}.', { - hpaLabel: HorizontalPodAutoscalerModel.label, - hpaName, - }), - ); - }); + return handlePromise(k8sKill(HorizontalPodAutoscalerModel, hpa)).then(() => { + close(); + }); }; return ( -
-
- - {' '} - {t('console-shared~Remove {{label}}?', { label: HorizontalPodAutoscalerModel.label })} - - + <> + + {' '} + {t('console-shared~Remove {{label}}?', { label: HorizontalPodAutoscalerModel.label })} + + } + /> + + {hpaName ? ( <>

@@ -70,20 +58,51 @@ const DeleteHPAModal: FC = ({ close, hpa, workload }) => {

) : ( - !submitError && + !errorMessage && )} -
- -
-
+ + + + + + + ); }; -export default createModalLauncher(DeleteHPAModal); +type DeleteHPAModalOverlayProps = { + hpa: HorizontalPodAutoscalerKind; + workload: K8sResourceCommon; +}; + +export const DeleteHPAModalOverlay: OverlayComponent = (props) => { + const [isOpen, setIsOpen] = useState(true); + const handleClose = () => { + setIsOpen(false); + props.closeOverlay(); + }; + + return isOpen ? ( + + + + ) : null; +}; + +export default DeleteHPAModalOverlay; diff --git a/frontend/packages/console-shared/src/components/hpa/index.ts b/frontend/packages/console-shared/src/components/hpa/index.ts index fc131d9b545..53549d32346 100644 --- a/frontend/packages/console-shared/src/components/hpa/index.ts +++ b/frontend/packages/console-shared/src/components/hpa/index.ts @@ -1 +1,10 @@ -export { default as deleteHPAModal } from './DeleteHPAModal'; +import { lazy } from 'react'; + +export { DeleteHPAModalOverlay } from './DeleteHPAModal'; + +// Lazy-loaded OverlayComponent for DeleteHPAModal +export const LazyDeleteHPAModalOverlay = lazy(() => + import('./DeleteHPAModal' /* webpackChunkName: "delete-hpa-modal" */).then((m) => ({ + default: m.DeleteHPAModalOverlay, + })), +); \ No newline at end of file diff --git a/frontend/packages/console-shared/src/components/index.ts b/frontend/packages/console-shared/src/components/index.ts index 8a4b3e379b6..c1fdd23fe00 100644 --- a/frontend/packages/console-shared/src/components/index.ts +++ b/frontend/packages/console-shared/src/components/index.ts @@ -15,7 +15,6 @@ export * from './virtualized-grid'; export * from './alerts'; export * from './popover'; export * from './utils'; -export * from './modal'; export * from './modals'; export * from './hpa'; export * from './multi-tab-list'; diff --git a/frontend/packages/console-shared/src/components/modal/Modal.tsx b/frontend/packages/console-shared/src/components/modal/Modal.tsx deleted file mode 100644 index 76d2388c6f3..00000000000 --- a/frontend/packages/console-shared/src/components/modal/Modal.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { LegacyRef, FC } from 'react'; -import type { ModalProps as PfModalProps } from '@patternfly/react-core/deprecated'; -import { Modal as PfModal } from '@patternfly/react-core/deprecated'; -import { css } from '@patternfly/react-styles'; -import './Modal.scss'; - -type ModalProps = { - isFullScreen?: boolean; - ref?: LegacyRef; -} & PfModalProps; - -const Modal: FC = ({ isFullScreen = false, className, ...props }) => ( - (isFullScreen ? document.body : document.querySelector('#modal-container'))} - /> -); - -export default Modal; diff --git a/frontend/packages/console-shared/src/components/modal/index.ts b/frontend/packages/console-shared/src/components/modal/index.ts deleted file mode 100644 index c6b35681c7e..00000000000 --- a/frontend/packages/console-shared/src/components/modal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Modal } from './Modal'; diff --git a/frontend/packages/console-shared/src/components/modals/ConsolePluginModal.tsx b/frontend/packages/console-shared/src/components/modals/ConsolePluginModal.tsx index 84fe9826978..958277bcff6 100644 --- a/frontend/packages/console-shared/src/components/modals/ConsolePluginModal.tsx +++ b/frontend/packages/console-shared/src/components/modals/ConsolePluginModal.tsx @@ -1,12 +1,7 @@ import { useState } from 'react'; -import { Form } from '@patternfly/react-core'; +import { Button, Form, Modal, ModalBody, ModalHeader, ModalVariant } from '@patternfly/react-core'; import { useTranslation } from 'react-i18next'; -import { - createModalLauncher, - ModalTitle, - ModalBody, - ModalSubmitFooter, -} from '@console/internal/components/factory/modal'; +import type { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; import { ConsoleOperatorConfigModel } from '@console/internal/models'; import type { K8sResourceKind } from '@console/internal/module/k8s'; import { k8sPatch } from '@console/internal/module/k8s'; @@ -16,6 +11,7 @@ import { } from '@console/shared/src/components/utils'; import { usePromiseHandler } from '@console/shared/src/hooks/promise-handler'; import { getPluginPatch, isPluginEnabled } from '@console/shared/src/utils'; +import { ModalFooterWithAlerts } from './ModalFooterWithAlerts'; export const ConsolePluginModal = (props: ConsolePluginModalProps) => { const { cancel, close, consoleOperatorConfig, csvPluginsCount, pluginName, trusted } = props; @@ -33,23 +29,25 @@ export const ConsolePluginModal = (props: ConsolePluginModalProps) => { }; return ( -
- - {csvPluginsCount > 1 - ? t('console-shared~Console plugin enablement - {{plugin}}', { plugin: pluginName }) - : t('console-shared~Console plugin enablement')} - + <> + 1 + ? t('console-shared~Console plugin enablement - {{plugin}}', { plugin: pluginName }) + : t('console-shared~Console plugin enablement') + } + /> -

- {csvPluginsCount - ? t( - 'console-shared~This operator includes a console plugin which provides a custom interface that can be included in the console. Updating the enablement of this console plugin will prompt for the console to be refreshed once it has been updated. Make sure you trust this console plugin before enabling.', - ) - : t( - 'console-shared~This console plugin provides a custom interface that can be included in the console. Updating the enablement of this console plugin will prompt for the console to be refreshed once it has been updated. Make sure you trust this console plugin before enabling.', - )} -

- + +

+ {csvPluginsCount + ? t( + 'console-shared~This operator includes a console plugin which provides a custom interface that can be included in the console. Updating the enablement of this console plugin will prompt for the console to be refreshed once it has been updated. Make sure you trust this console plugin before enabling.', + ) + : t( + 'console-shared~This console plugin provides a custom interface that can be included in the console. Updating the enablement of this console plugin will prompt for the console to be refreshed once it has been updated. Make sure you trust this console plugin before enabling.', + )} +

{ />
- - + + + + + ); }; -export const consolePluginModal = createModalLauncher(ConsolePluginModal); +type ConsolePluginModalProviderProps = { + consoleOperatorConfig: K8sResourceKind; + csvPluginsCount?: number; + pluginName: string; + trusted: boolean; +}; + +export const ConsolePluginModalOverlay: OverlayComponent = ( + props, +) => { + const [isOpen, setIsOpen] = useState(true); + const handleClose = () => { + setIsOpen(false); + props.closeOverlay(); + }; + + return isOpen ? ( + + + + ) : null; +}; + +export default ConsolePluginModalOverlay; export type ConsolePluginModalProps = { consoleOperatorConfig: K8sResourceKind; diff --git a/frontend/packages/console-shared/src/components/modals/CreateNamespaceModal.tsx b/frontend/packages/console-shared/src/components/modals/CreateNamespaceModal.tsx index a444efba571..d87252ece1e 100644 --- a/frontend/packages/console-shared/src/components/modals/CreateNamespaceModal.tsx +++ b/frontend/packages/console-shared/src/components/modals/CreateNamespaceModal.tsx @@ -169,7 +169,7 @@ export const CreateNamespaceModal: ModalComponent = ({ , + + + ); }; const DeleteResourceModal: FC = (props) => { const [handlePromise] = usePromiseHandler(); + const navigate = useNavigate(); const handleSubmit = (values: FormikValues, actions) => { const { onSubmit, close, redirect } = props; @@ -83,7 +91,9 @@ const DeleteResourceModal: FC = (props) => { handlePromise(onSubmit(values)) .then(() => { close(); - redirect && history.push(redirect); + if (redirect) { + navigate(redirect); + } }) .catch((errorMessage) => { actions.setStatus({ submitError: errorMessage }); @@ -102,6 +112,34 @@ const DeleteResourceModal: FC = (props) => { ); }; -export const deleteResourceModal = createModalLauncher((props: DeleteResourceModalProps) => ( - -)); +export const DeleteResourceModalOverlay: OverlayComponent = (props) => { + const [isOpen, setIsOpen] = useState(true); + const handleClose = () => { + setIsOpen(false); + props.closeOverlay(); + }; + + return isOpen ? ( + + + + ) : null; +}; + +type DeleteResourceModalProps = ModalComponentProps & { + resourceName: string; + resourceType: string; + actionLabel?: string; // Used to send translated strings as action label. + actionLabelKey?: string; // Used to send translation key for action label. + redirect?: string; + onSubmit: (values: FormikValues) => Promise; +}; diff --git a/frontend/packages/console-shared/src/components/modals/ModalFooterWithAlerts.tsx b/frontend/packages/console-shared/src/components/modals/ModalFooterWithAlerts.tsx new file mode 100644 index 00000000000..2d415a5f4d2 --- /dev/null +++ b/frontend/packages/console-shared/src/components/modals/ModalFooterWithAlerts.tsx @@ -0,0 +1,30 @@ +import { AlertGroup, Flex, ModalFooter } from '@patternfly/react-core'; +import { ErrorMessage, InfoMessage } from '@console/internal/components/utils/button-bar'; + +export const ModalFooterWithAlerts: React.FC = ({ + children, + errorMessage, + message, +}) => ( + + {(errorMessage || message) && ( + + {errorMessage && } + {message && } + + )} + {children} + +); + +type ModalFooterWithAlertsProps = { + children: React.ReactNode; + message?: string; + errorMessage?: string; +}; diff --git a/frontend/packages/console-shared/src/components/modals/index.ts b/frontend/packages/console-shared/src/components/modals/index.ts index ac902b242de..34e9052ccba 100644 --- a/frontend/packages/console-shared/src/components/modals/index.ts +++ b/frontend/packages/console-shared/src/components/modals/index.ts @@ -1,9 +1,14 @@ -export const consolePluginModal = (props) => - import('./ConsolePluginModal' /* webpackChunkName: "shared-modals" */).then((m) => - m.consolePluginModal(props), - ); +import { lazy } from 'react'; -export const deleteResourceModal = (props) => - import('./DeleteResourceModal' /* webpackChunkName: "shared-modals" */).then((m) => - m.deleteResourceModal(props), - ); +// Lazy-loaded OverlayComponent for ConsolePluginModal +export const LazyConsolePluginModalOverlay = lazy(() => + import('./ConsolePluginModal' /* webpackChunkName: "shared-modals" */).then((m) => ({ + default: m.ConsolePluginModalOverlay, + })), +); + +export const LazyDeleteResourceModalOverlay = lazy(() => + import('./DeleteResourceModal' /* webpackChunkName: "delete-resource-modal" */).then((m) => ({ + default: m.DeleteResourceModalOverlay, + })), +); diff --git a/frontend/packages/console-shared/src/components/multi-tab-list/MultiTabListPage.tsx b/frontend/packages/console-shared/src/components/multi-tab-list/MultiTabListPage.tsx index 0ce20da6acd..498cd2a44a5 100644 --- a/frontend/packages/console-shared/src/components/multi-tab-list/MultiTabListPage.tsx +++ b/frontend/packages/console-shared/src/components/multi-tab-list/MultiTabListPage.tsx @@ -2,10 +2,9 @@ import type { ReactNode, FC } from 'react'; import { ActionListItem, Button } from '@patternfly/react-core'; import { SimpleDropdown } from '@patternfly/react-templates'; import { useTranslation } from 'react-i18next'; -import { useParams, Link } from 'react-router-dom-v5-compat'; +import { useParams, Link, useNavigate } from 'react-router-dom-v5-compat'; import type { Page } from '@console/internal/components/utils/horizontal-nav'; import { HorizontalNav } from '@console/internal/components/utils/horizontal-nav'; -import { history } from '@console/internal/components/utils/router'; import { referenceForModel } from '@console/internal/module/k8s'; import { PageHeading } from '@console/shared/src/components/heading/PageHeading'; import { PageTitleContext } from '../pagetitle/PageTitleContext'; @@ -29,6 +28,7 @@ const MultiTabListPage: FC = ({ telemetryPrefix, }) => { const { t } = useTranslation(); + const navigate = useNavigate(); const { ns } = useParams(); const onSelectCreateAction = (actionName: string): void => { const selectedMenuItem: MenuAction = menuActions[actionName]; @@ -42,7 +42,7 @@ const MultiTabListPage: FC = ({ url = selectedMenuItem.onSelection(actionName, selectedMenuItem, url); } if (url) { - history.push(url); + navigate(url); } }; diff --git a/frontend/packages/console-shared/src/hooks/useCopyCodeModal.tsx b/frontend/packages/console-shared/src/hooks/useCopyCodeModal.tsx index 0c2b342fa36..e1dd798afa1 100644 --- a/frontend/packages/console-shared/src/hooks/useCopyCodeModal.tsx +++ b/frontend/packages/console-shared/src/hooks/useCopyCodeModal.tsx @@ -1,12 +1,15 @@ import { useCallback } from 'react'; +import { Modal, ModalHeader, ModalBody, ModalVariant } from '@patternfly/react-core'; import { useModal } from '@console/dynamic-plugin-sdk/src/lib-core'; import { CopyToClipboard } from '@console/internal/components/utils/copy-to-clipboard'; -import { Modal } from '@console/shared/src/components/modal'; import type { ModalComponent } from 'packages/console-dynamic-plugin-sdk/src/app/modal-support/ModalProvider'; const CopyCodeModal: CopyCodeModalComponent = ({ title, snippet, closeModal }) => ( - - + + + + + ); diff --git a/frontend/packages/dev-console/src/actions/context-menu.ts b/frontend/packages/dev-console/src/actions/context-menu.ts index 70ea9158bbc..ae01022e86f 100644 --- a/frontend/packages/dev-console/src/actions/context-menu.ts +++ b/frontend/packages/dev-console/src/actions/context-menu.ts @@ -1,5 +1,4 @@ import { useMemo } from 'react'; -import i18next from 'i18next'; import { useTranslation } from 'react-i18next'; import type { Action, K8sModel } from '@console/dynamic-plugin-sdk'; import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; @@ -7,36 +6,44 @@ import type { TopologyApplicationObject } from '@console/dynamic-plugin-sdk/src/ import { LazyDeleteModalOverlay } from '@console/internal/components/modals'; import { asAccessReview } from '@console/internal/components/utils'; import type { K8sResourceKind } from '@console/internal/module/k8s'; -import { deleteResourceModal } from '@console/shared'; +import { LazyDeleteResourceModalOverlay } from '@console/shared'; import { ApplicationModel } from '@console/topology/src/models'; import { cleanUpWorkload } from '@console/topology/src/utils'; -export const DeleteApplicationAction = ( +export const useDeleteApplicationAction = ( application: TopologyApplicationObject, resourceModel: K8sModel, ): Action => { - // accessReview needs a resource but group is not a k8s resource, - // so currently picking the first resource to do the rbac checks (might change in future) - const primaryResource = application.resources[0].resource; - return { - id: 'delete-application', - label: i18next.t('devconsole~Delete application'), - cta: () => { - const reqs = []; - deleteResourceModal({ - blocking: true, - resourceName: application.name, - resourceType: ApplicationModel.label, - onSubmit: () => { - application.resources.forEach((resource) => { - reqs.push(cleanUpWorkload(resource.resource)); - }); - return Promise.all(reqs); - }, - }); - }, - accessReview: asAccessReview(resourceModel, primaryResource, 'delete'), - }; + const { t } = useTranslation(); + const launchModal = useOverlay(); + + return useMemo(() => { + if (!application?.resources?.[0]?.resource) { + return null; + } + + // accessReview needs a resource but group is not a k8s resource, + // so currently picking the first resource to do the rbac checks (might change in future) + const primaryResource = application.resources[0].resource; + return { + id: 'delete-application', + label: t('devconsole~Delete application'), + cta: () => { + const reqs = []; + launchModal(LazyDeleteResourceModalOverlay, { + resourceName: application.name, + resourceType: ApplicationModel.label, + onSubmit: () => { + application.resources.forEach((resource) => { + reqs.push(cleanUpWorkload(resource.resource)); + }); + return Promise.all(reqs); + }, + }); + }, + accessReview: asAccessReview(resourceModel, primaryResource, 'delete'), + }; + }, [application, resourceModel, t, launchModal]); }; export const useDeleteResourceAction = ( diff --git a/frontend/packages/dev-console/src/actions/providers.tsx b/frontend/packages/dev-console/src/actions/providers.tsx index 8bee15c8395..affa549c1d5 100644 --- a/frontend/packages/dev-console/src/actions/providers.tsx +++ b/frontend/packages/dev-console/src/actions/providers.tsx @@ -34,7 +34,7 @@ import { ADD_TO_PROJECT, } from '../const'; import { AddActions, disabledActionsFilter } from './add-resources'; -import { DeleteApplicationAction } from './context-menu'; +import { useDeleteApplicationAction } from './context-menu'; import { EditImportApplication } from './creators'; type TopologyActionProvider = (data: { @@ -295,6 +295,7 @@ export const useTopologyApplicationActionProvider: TopologyActionProvider = ({ ); const primaryResource = appData.resources?.[0]?.resource || {}; const [kindObj, inFlight] = useK8sModel(referenceFor(primaryResource)); + const deleteApplicationAction = useDeleteApplicationAction(appData, kindObj); return useMemo(() => { if (element.getType() === TYPE_APPLICATION_GROUP) { @@ -305,7 +306,7 @@ export const useTopologyApplicationActionProvider: TopologyActionProvider = ({ ? `${referenceFor(sourceObj)}/${sourceObj?.metadata?.name}` : undefined; const actions = [ - ...(connectorSource ? [] : [DeleteApplicationAction(appData, kindObj)]), + ...(connectorSource ? [] : [deleteApplicationAction]), AddActions.FromGit(namespace, application, sourceReference, path, !isImportResourceAccess), AddActions.ContainerImage( namespace, @@ -352,13 +353,12 @@ export const useTopologyApplicationActionProvider: TopologyActionProvider = ({ element, inFlight, connectorSource, - appData, - kindObj, namespace, application, isImportResourceAccess, isCatalogImageResourceAccess, isServerlessEnabled, isJavaImageStreamEnabled, + deleteApplicationAction, ]); }; diff --git a/frontend/packages/helm-plugin/src/actions/creators.ts b/frontend/packages/helm-plugin/src/actions/creators.ts index a17bc43e5ec..5c8e2350071 100644 --- a/frontend/packages/helm-plugin/src/actions/creators.ts +++ b/frontend/packages/helm-plugin/src/actions/creators.ts @@ -1,39 +1,53 @@ +import { useMemo } from 'react'; import type { TFunction } from 'i18next'; import type { Action, K8sKind } from '@console/dynamic-plugin-sdk'; +import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; import { coFetchJSON } from '@console/internal/co-fetch'; import type { K8sResourceKind } from '@console/internal/module/k8s'; import { referenceFor } from '@console/internal/module/k8s'; -import { deleteResourceModal } from '@console/shared'; +import { LazyDeleteResourceModalOverlay } from '@console/shared'; import { ProjectHelmChartRepositoryModel } from '../models'; import type { HelmActionsScope } from './types'; -export const getHelmDeleteAction = ( - { - release: { name: releaseName, namespace, version: releaseVersion }, - redirect, - }: HelmActionsScope, - t: TFunction, -): Action => ({ - id: 'delete-helm', - label: t('helm-plugin~Delete Helm Release'), - cta: () => { - deleteResourceModal({ - blocking: true, - resourceName: releaseName, - resourceType: 'Helm Release', - actionLabel: t('helm-plugin~Delete'), +export const useHelmDeleteAction = (scope: HelmActionsScope, t: TFunction): Action => { + const launchModal = useOverlay(); + + return useMemo(() => { + if (!scope?.release) { + return { + id: 'delete-helm', + label: t('helm-plugin~Delete Helm Release'), + cta: () => {}, + }; + } + + const { + release: { name: releaseName, namespace, version: releaseVersion }, redirect, - onSubmit: () => { - return coFetchJSON.delete( - `/api/helm/release/async?name=${releaseName}&ns=${namespace}&version=${releaseVersion}`, - null, - null, - -1, - ); + } = scope; + + return { + id: 'delete-helm', + label: t('helm-plugin~Delete Helm Release'), + cta: () => { + launchModal(LazyDeleteResourceModalOverlay, { + resourceName: releaseName, + resourceType: 'Helm Release', + actionLabel: t('helm-plugin~Delete'), + redirect, + onSubmit: () => { + return coFetchJSON.delete( + `/api/helm/release/async?name=${releaseName}&ns=${namespace}&version=${releaseVersion}`, + null, + null, + -1, + ); + }, + }); }, - }); - }, -}); + }; + }, [scope, t, launchModal]); +}; export const getHelmUpgradeAction = ( { release: { name: releaseName, namespace }, actionOrigin }: HelmActionsScope, diff --git a/frontend/packages/helm-plugin/src/actions/providers.ts b/frontend/packages/helm-plugin/src/actions/providers.ts index 3f699626072..418702d76ee 100644 --- a/frontend/packages/helm-plugin/src/actions/providers.ts +++ b/frontend/packages/helm-plugin/src/actions/providers.ts @@ -20,7 +20,7 @@ import { TYPE_HELM_RELEASE } from '../topology/components/const'; import { HelmReleaseStatus } from '../types/helm-types'; import { AddHelmChartAction } from './add-resources'; import { - getHelmDeleteAction, + useHelmDeleteAction, getHelmRollbackAction, getHelmUpgradeAction, editChartRepository, @@ -29,25 +29,26 @@ import type { HelmActionsScope } from './types'; export const useHelmActionProvider = (scope: HelmActionsScope) => { const { t } = useTranslation(); + const helmDeleteAction = useHelmDeleteAction(scope, t); const result = useMemo(() => { if (!scope) return [[], true, undefined]; switch (scope?.release?.info?.status) { case HelmReleaseStatus.PendingInstall: case HelmReleaseStatus.PendingRollback: case HelmReleaseStatus.PendingUpgrade: - return [[getHelmDeleteAction(scope, t)], true, undefined]; + return [[helmDeleteAction], true, undefined]; default: return [ [ getHelmUpgradeAction(scope, t), ...(Number(scope.release.version) > 1 ? [getHelmRollbackAction(scope, t)] : []), - getHelmDeleteAction(scope, t), + helmDeleteAction, ], true, undefined, ]; } - }, [scope, t]); + }, [scope, t, helmDeleteAction]); return result; }; diff --git a/frontend/packages/helm-plugin/src/actions/types.ts b/frontend/packages/helm-plugin/src/actions/types.ts index 0cd4b2fc537..45caa47af46 100644 --- a/frontend/packages/helm-plugin/src/actions/types.ts +++ b/frontend/packages/helm-plugin/src/actions/types.ts @@ -10,5 +10,5 @@ type HelmActionObj = { export type HelmActionsScope = { release: HelmRelease | HelmActionObj; actionOrigin?: string; - redirect?: boolean; + redirect?: string; }; diff --git a/frontend/packages/knative-plugin/src/components/test-function/TestFunctionModal.tsx b/frontend/packages/knative-plugin/src/components/test-function/TestFunctionModal.tsx index 36371cdfd7a..cf0f5b101c2 100644 --- a/frontend/packages/knative-plugin/src/components/test-function/TestFunctionModal.tsx +++ b/frontend/packages/knative-plugin/src/components/test-function/TestFunctionModal.tsx @@ -44,7 +44,7 @@ const TestFunctionModal: FC = (props) => { {t('knative-plugin~Test')}     - @@ -64,7 +64,7 @@ const TestFunctionModal: FC = (props) => {   - diff --git a/frontend/packages/metal3-plugin/src/components/modals/RestartHostModal.tsx b/frontend/packages/metal3-plugin/src/components/modals/RestartHostModal.tsx index 5c0aa2f8048..802271bfdd1 100644 --- a/frontend/packages/metal3-plugin/src/components/modals/RestartHostModal.tsx +++ b/frontend/packages/metal3-plugin/src/components/modals/RestartHostModal.tsx @@ -56,7 +56,7 @@ const RestartHostModal: OverlayComponent = (props) => { - diff --git a/frontend/packages/metal3-plugin/src/components/modals/StartNodeMaintenanceModal.tsx b/frontend/packages/metal3-plugin/src/components/modals/StartNodeMaintenanceModal.tsx index 288889d01ff..52b47a2858d 100644 --- a/frontend/packages/metal3-plugin/src/components/modals/StartNodeMaintenanceModal.tsx +++ b/frontend/packages/metal3-plugin/src/components/modals/StartNodeMaintenanceModal.tsx @@ -105,7 +105,7 @@ export const StartNodeMaintenanceModal: OverlayComponent {t('metal3-plugin~Start Maintenance')} - diff --git a/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.tsx b/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.tsx index 8429d635d4b..3b3ae1658ec 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.tsx @@ -30,6 +30,7 @@ import { useAccessReviewAllowed, useAccessReview, } from '@console/dynamic-plugin-sdk'; +import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; import { getGroupVersionKindForModel } from '@console/dynamic-plugin-sdk/src/lib-core'; import { Conditions, ConditionTypes } from '@console/internal/components/conditions'; import { ResourceEventStream } from '@console/internal/components/events'; @@ -66,7 +67,7 @@ import { DocumentTitle } from '@console/shared/src/components/document-title/Doc import { withFallback } from '@console/shared/src/components/error'; import PaneBody from '@console/shared/src/components/layout/PaneBody'; import { ExternalLink } from '@console/shared/src/components/links/ExternalLink'; -import { consolePluginModal } from '@console/shared/src/components/modals'; +import { LazyConsolePluginModalOverlay } from '@console/shared/src/components/modals'; import { RedExclamationCircleIcon } from '@console/shared/src/components/status/icons'; import { CONSOLE_OPERATOR_CONFIG_NAME } from '@console/shared/src/constants'; import { useActiveNamespace } from '@console/shared/src/hooks/redux-selectors'; @@ -228,6 +229,7 @@ const ManagedNamespaces: FC = ({ obj }) => { }; const ConsolePlugins: FC = ({ csvPlugins, trusted }) => { + const launchModal = useOverlay(); const console: WatchK8sResource = { kind: referenceForModel(ConsoleOperatorConfigModel), isList: false, @@ -262,7 +264,7 @@ const ConsolePlugins: FC = ({ csvPlugins, trusted }) => { type="button" isInline onClick={() => - consolePluginModal({ + launchModal(LazyConsolePluginModalOverlay, { consoleOperatorConfig, pluginName, trusted, diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx index 443ab4338b1..32406693a97 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx @@ -8,6 +8,9 @@ import { EmptyStateFooter, EmptyStateVariant, Truncate, + Modal, + ModalBody, + ModalHeader, } from '@patternfly/react-core'; import { css } from '@patternfly/react-styles'; import * as _ from 'lodash'; @@ -19,7 +22,6 @@ import { TileViewPage } from '@console/internal/components/utils/tile-view-page' import i18n from '@console/internal/i18n'; import { GreenCheckCircleIcon, - Modal, COMMUNITY_PROVIDERS_WARNING_LOCAL_STORAGE_KEY as storeKey, COMMUNITY_PROVIDERS_WARNING_USERSETTINGS_KEY as userSettingsKey, useUserSettingsCompatibility, @@ -42,8 +44,9 @@ import { sourceSort, validSubscriptionSort, } from './operator-hub-utils'; -import type { OperatorHubItem, TokenizedAuthProvider } from './index'; import { InfrastructureFeature } from './index'; +import type { OperatorHubItem, TokenizedAuthProvider } from './index'; +import '@console/shared/src/components/catalog/details/CatalogDetailsModal.scss'; // Scoring and priority code no longer used and will be removed with Operator Hub catalog files cleanup effort const SCORE = { @@ -1038,65 +1041,64 @@ export const OperatorHubTileView: FC = (props) => { /> {detailsItem && ( - - -
- {!detailsItem.installed ? ( - - {t('olm~Install')} - - ) : ( - - )} -
- - } > - + + + +
+ {!detailsItem.installed ? ( + + {t('olm~Install')} + + ) : ( + + )} +
+
+ + +
)} diff --git a/frontend/packages/operator-lifecycle-manager/src/components/registry-poll-interval-details.tsx b/frontend/packages/operator-lifecycle-manager/src/components/registry-poll-interval-details.tsx index 9fdbac45f6a..66075df5c91 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/registry-poll-interval-details.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/registry-poll-interval-details.tsx @@ -143,7 +143,7 @@ export const RegistryPollIntervalDetailItem: FC - - + +
); }; diff --git a/frontend/public/components/modals/configure-machine-autoscaler-modal.tsx b/frontend/public/components/modals/configure-machine-autoscaler-modal.tsx index 447c9a79520..363319c5467 100644 --- a/frontend/public/components/modals/configure-machine-autoscaler-modal.tsx +++ b/frontend/public/components/modals/configure-machine-autoscaler-modal.tsx @@ -2,17 +2,7 @@ import { useState, useCallback } from 'react'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom-v5-compat'; -import { - Modal, - ModalHeader, - ModalBody, - ModalFooter, - Button, - HelperText, - HelperTextItem, - FormGroup, - Form, -} from '@patternfly/react-core'; +import { Modal, ModalHeader, ModalBody, Button, FormGroup, Form } from '@patternfly/react-core'; import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; import { MachineAutoscalerModel } from '../../models'; import { NumberSpinner } from '../utils/number-spinner'; @@ -20,6 +10,7 @@ import { resourcePathFromModel } from '../utils/resource-link'; import { K8sResourceKind } from '../../module/k8s'; import { k8sCreateResource } from '@console/dynamic-plugin-sdk/src/utils/k8s'; import { usePromiseHandler } from '@console/shared/src/hooks/promise-handler'; +import { ModalFooterWithAlerts } from '@console/shared/src/components/modals/ModalFooterWithAlerts'; export const ConfigureMachineAutoscalerModal: OverlayComponent = ({ machineSet, @@ -116,7 +107,7 @@ export const ConfigureMachineAutoscalerModal: OverlayComponent -
+ - {errorMessage && ( - - {errorMessage} - - )}
- - - - + + ); }; diff --git a/frontend/public/components/modals/delete-modal.tsx b/frontend/public/components/modals/delete-modal.tsx index d7023fb00c3..1d0320478cb 100644 --- a/frontend/public/components/modals/delete-modal.tsx +++ b/frontend/public/components/modals/delete-modal.tsx @@ -1,17 +1,19 @@ import * as _ from 'lodash'; import type { ReactNode } from 'react'; import { useState, useCallback, useEffect } from 'react'; -import { Alert, Checkbox } from '@patternfly/react-core'; +import { + Alert, + Button, + Checkbox, + Modal, + ModalBody, + ModalHeader, + ModalVariant, +} from '@patternfly/react-core'; import { Trans, useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom-v5-compat'; import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; -import { - ModalTitle, - ModalBody, - ModalSubmitFooter, - ModalWrapper, - ModalComponentProps, -} from '../factory/modal'; +import { ModalComponentProps } from '../factory/modal'; import { resourceListPathFromModel, ResourceLink } from '../utils/resource-link'; import { k8sKill, @@ -27,6 +29,7 @@ import { findOwner } from '../../module/k8s/managed-by'; import { LocationDescriptor } from 'history'; import { usePromiseHandler } from '@console/shared/src/hooks/promise-handler'; +import { ModalFooterWithAlerts } from '@console/shared/src/components/modals/ModalFooterWithAlerts'; //Modal for resource deletion and allows cascading deletes if propagationPolicy is provided for the enum export const DeleteModal = (props: DeleteModalProps) => { @@ -87,93 +90,116 @@ export const DeleteModal = (props: DeleteModalProps) => { }); const { kind, resource, message } = props; + return ( -
- - {' '} - {t('public~Delete {{kind}}?', { - kind: kind ? (kind.labelKey ? t(kind.labelKey) : kind.label) : '', - })} - - - {message} -
- {_.has(resource.metadata, 'namespace') ? ( - - Are you sure you want to delete{' '} - - {{ resourceName: resource?.metadata?.name }} - {' '} - in namespace {{ namespace: resource?.metadata?.namespace }}? - - ) : ( - - Are you sure you want to delete{' '} - - {{ resourceName: resource?.metadata?.name }} - - ? - - )} - {_.has(kind, 'propagationPolicy') && ( - setIsChecked(checked)} - isChecked={isChecked} - name="deleteDependentObjects" - id="deleteDependentObjects" - /> - )} - {props.deleteAllResources && ( - setIsDeleteOtherResourcesChecked(checked)} - isChecked={isDeleteOtherResourcesChecked} - name="deleteOtherResources" - id="deleteOtherResources" - /> - )} - {owner && ( - + <> + + {' '} + {t('public~Delete {{kind}}?', { + kind: kind ? (kind.labelKey ? t(kind.labelKey) : kind.label) : '', + })} + + } + data-test-id="modal-title" + /> + + + {message} +
+ {_.has(resource.metadata, 'namespace') ? ( - This resource is managed by{' '} - {' '} - and any modifications may be overwritten. Edit the managing resource to preserve - changes. + Are you sure you want to delete{' '} + + {{ resourceName: resource?.metadata?.name }} + {' '} + in namespace {{ namespace: resource?.metadata?.namespace }}? - - )} -
+ ) : ( + + Are you sure you want to delete{' '} + + {{ resourceName: resource?.metadata?.name }} + + ? + + )} + {_.has(kind, 'propagationPolicy') && ( + setIsChecked(checked)} + isChecked={isChecked} + name="deleteDependentObjects" + id="deleteDependentObjects" + /> + )} + {props.deleteAllResources && ( + setIsDeleteOtherResourcesChecked(checked)} + isChecked={isDeleteOtherResourcesChecked} + name="deleteOtherResources" + id="deleteOtherResources" + /> + )} + {owner && ( + + + This resource is managed by{' '} + {' '} + and any modifications may be overwritten. Edit the managing resource to preserve + changes. + + + )} +
+
- - + + + + + ); }; export const DeleteModalOverlay: OverlayComponent = (props) => { - return ( - - - - ); + const [isOpen, setIsOpen] = useState(true); + const handleClose = () => { + setIsOpen(false); + props.closeOverlay(); + }; + + return isOpen ? ( + + + + ) : null; }; export type DeleteModalProps = { diff --git a/frontend/public/components/modals/delete-namespace-modal.tsx b/frontend/public/components/modals/delete-namespace-modal.tsx index 667b11365fc..1038373ce3e 100644 --- a/frontend/public/components/modals/delete-namespace-modal.tsx +++ b/frontend/public/components/modals/delete-namespace-modal.tsx @@ -120,7 +120,7 @@ export const DeleteNamespaceModal: OverlayComponent = > {t('public~Delete')} - diff --git a/frontend/public/components/secrets/create-secret/SecretFormWrapper.tsx b/frontend/public/components/secrets/create-secret/SecretFormWrapper.tsx index 1a7d50ca467..b0e056d82bc 100644 --- a/frontend/public/components/secrets/create-secret/SecretFormWrapper.tsx +++ b/frontend/public/components/secrets/create-secret/SecretFormWrapper.tsx @@ -205,7 +205,7 @@ export const SecretFormWrapper: FC = (props) => { > {props.saveButtonText || t('public~Create')} - diff --git a/frontend/public/components/utils/button-bar.tsx b/frontend/public/components/utils/button-bar.tsx index 23ffe0c55b3..1cc41c9d64a 100644 --- a/frontend/public/components/utils/button-bar.tsx +++ b/frontend/public/components/utils/button-bar.tsx @@ -29,7 +29,7 @@ export const ErrorMessage = ({ message }) => { ); }; -const InfoMessage = ({ message }) => ( +export const InfoMessage = ({ message }) => (