Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 59 additions & 65 deletions frontend/packages/console-app/src/actions/creators/hpa-factory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMemo } from 'react';
import i18next from 'i18next';
import { 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 {
Expand All @@ -14,84 +15,23 @@ import {
ClusterServiceVersionModel,
ClusterServiceVersionKind,
} from '@console/operator-lifecycle-manager';
import deleteHPAModal from '@console/shared/src/components/hpa/DeleteHPAModal';
import DeleteHPAModalProvider from '@console/shared/src/components/hpa/DeleteHPAModal';
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 { 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 launchOverlay = useOverlay();
const namespace = resource?.metadata?.namespace;

const watchedResources = useMemo(
Expand Down Expand Up @@ -123,9 +63,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<Action[]>(() => {
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: () => {
launchOverlay(DeleteHPAModalProvider, {
workload: resource,
hpa: relatedHPAs[0],
});
},
Comment on lines +102 to +107
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for DeleteHPAModal usage and related patterns
rg -n -C5 'deleteHPAModal|DeleteHPAModal' --type=ts --type=tsx

Repository: openshift/console

Length of output: 88


🏁 Script executed:

# Look at the hpa-factory.ts file to understand the relatedHPAs population
cat -n frontend/packages/console-app/src/actions/creators/hpa-factory.ts | head -150

Repository: openshift/console

Length of output: 5067


🏁 Script executed:

# Check for any comments or tests about multiple HPA scenarios
rg -n -C3 'relatedHPAs|multiple.*HPA|HPA.*multiple' --type=ts --type=tsx

Repository: openshift/console

Length of output: 88


🏁 Script executed:

# Find and examine the doesHpaMatch function
rg -n -C10 'export.*doesHpaMatch|function.*doesHpaMatch' --type=ts

Repository: openshift/console

Length of output: 1237


🏁 Script executed:

# Look at the DeleteHPAModal component
find . -name 'DeleteHPAModal*' -type f | head -5

Repository: openshift/console

Length of output: 133


🏁 Script executed:

# Search for any tests or comments about multiple HPAs
rg -n 'multiple.*HPA|HPA.*multiple|relatedHPAs.*length|relatedHPAs\[' --type=ts | head -20

Repository: openshift/console

Length of output: 586


🏁 Script executed:

# Check if there are any related issues or TODOs
rg -n 'TODO|FIXME|BUG' frontend/packages/console-app/src/actions/creators/hpa-factory.ts

Repository: openshift/console

Length of output: 43


🏁 Script executed:

# Read the DeleteHPAModal component
cat -n ./frontend/packages/console-shared/src/components/hpa/DeleteHPAModal.tsx

Repository: openshift/console

Length of output: 4439


🏁 Script executed:

# Search for test files related to HPA
fd -e test.ts -e spec.ts hpa | head -20

Repository: openshift/console

Length of output: 208


🏁 Script executed:

# Check for tests in hpa-factory.ts
rg -n 'test|describe|it(' frontend/packages/console-app/src/actions/creators/ | grep -i hpa

Repository: openshift/console

Length of output: 145


🏁 Script executed:

# Check the HPA utils tests
cat -n frontend/packages/console-shared/src/utils/__tests__/hpa-utils.spec.ts

Repository: openshift/console

Length of output: 728


🏁 Script executed:

# Also check the dev-console HPA tests
cat -n frontend/packages/dev-console/src/components/hpa/__tests__/hpa-utils.spec.ts | head -100

Repository: openshift/console

Length of output: 5392


🏁 Script executed:

# Search for any architectural decisions or comments about multiple HPAs
rg -n 'multiple|single|one.*HPA|HPA.*one' frontend/packages/console-shared/src/utils/hpa-utils.ts frontend/packages/console-shared/src/components/hpa/DeleteHPAModal.tsx

Repository: openshift/console

Length of output: 350


Handle multiple HPAs targeting the same workload in the delete action.

The delete action passes relatedHPAs[0] to the DeleteHPAModal without checking if multiple HPAs target the workload. While Kubernetes discourages it, the API allows multiple HPAs per workload. When this occurs, only the first HPA can be deleted through the UI—users with multiple HPAs won't be able to remove the others. Either add a selection mechanism when multiple HPAs are present, warn the user, or document this as an unsupported edge case.

🤖 Prompt for AI Agents
In `@frontend/packages/console-app/src/actions/creators/hpa-factory.ts` around
lines 102 - 107, The delete CTA currently always passes only relatedHPAs[0] to
DeleteHPAModalProvider, which prevents deleting additional HPAs; modify the cta
in hpa-factory.ts to check relatedHPAs.length and when >1 either open a selector
(e.g., launch a small picker overlay listing relatedHPAs so the user can choose
which HPA to delete) or pass the entire relatedHPAs array to a revised
DeleteHPAModalProvider that shows a list with delete actions, and when length
=== 1 continue to pass relatedHPAs[0]; update references to relatedHPAs,
launchOverlay, and DeleteHPAModalProvider accordingly.

accessReview: {
group: HorizontalPodAutoscalerModel.apiGroup,
resource: HorizontalPodAutoscalerModel.plural,
namespace: resource.metadata?.namespace,
verb: 'delete',
},
},
];
// missing launchModal dependency, that 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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { useLocation } from 'react-router-dom-v5-compat';
import { useAccessReview, WatchK8sResource } from '@console/dynamic-plugin-sdk';
import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay';
import {
getGroupVersionKindForModel,
getReferenceForModel,
Expand All @@ -43,7 +44,7 @@ import {
import { 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 ConsolePluginModalProvider from '@console/shared/src/components/modals/ConsolePluginModal';
import {
GreenCheckCircleIcon,
YellowExclamationTriangleIcon,
Expand Down Expand Up @@ -97,6 +98,7 @@ export const ConsolePluginEnabledStatus: FC<ConsolePluginEnabledStatusProps> = (
enabled,
}) => {
const { t } = useTranslation();
const launchOverlay = useOverlay();

const {
consoleOperatorConfig,
Expand All @@ -116,7 +118,7 @@ export const ConsolePluginEnabledStatus: FC<ConsolePluginEnabledStatusProps> = (
type="button"
isInline
onClick={() =>
consolePluginModal({
launchOverlay(ConsolePluginModalProvider, {
consoleOperatorConfig,
pluginName,
trusted: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ 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 { useTranslation } from 'react-i18next';
import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider';
import {
createModalLauncher,
ModalBody,
ModalComponentProps,
ModalSubmitFooter,
ModalTitle,
ModalWrapper,
} from '@console/internal/components/factory/modal';
import { LoadingInline } from '@console/internal/components/utils/status-box';
import { HorizontalPodAutoscalerModel } from '@console/internal/models';
Expand All @@ -24,7 +25,7 @@ type DeleteHPAModalProps = ModalComponentProps & {
workload: K8sResourceCommon;
};

const DeleteHPAModal: FC<DeleteHPAModalProps> = ({ close, hpa, workload }) => {
const DeleteHPAModal: FC<DeleteHPAModalProps> = ({ close, cancel, hpa, workload }) => {
const [submitError, setSubmitError] = useState<string>(null);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const { t } = useTranslation();
Expand Down Expand Up @@ -82,11 +83,29 @@ const DeleteHPAModal: FC<DeleteHPAModalProps> = ({ close, hpa, workload }) => {
submitText={t('console-shared~Remove')}
submitDanger
submitDisabled={!!submitError}
cancel={close}
cancel={cancel}
/>
</div>
</Form>
);
};

export default createModalLauncher(DeleteHPAModal);
type DeleteHPAModalProviderProps = {
hpa: HorizontalPodAutoscalerKind;
workload: K8sResourceCommon;
};

const DeleteHPAModalProvider: OverlayComponent<DeleteHPAModalProviderProps> = (props) => {
return (
<ModalWrapper blocking onClose={props.closeOverlay}>
<DeleteHPAModal
close={props.closeOverlay}
cancel={props.closeOverlay}
hpa={props.hpa}
workload={props.workload}
/>
</ModalWrapper>
);
};

export default DeleteHPAModalProvider;
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { default as deleteHPAModal } from './DeleteHPAModal';
export { default as DeleteHPAModalProvider } from './DeleteHPAModal';
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useState } from 'react';
import { Form } from '@patternfly/react-core';
import { useTranslation } from 'react-i18next';
import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider';
import {
createModalLauncher,
ModalTitle,
ModalBody,
ModalSubmitFooter,
ModalWrapper,
} from '@console/internal/components/factory/modal';
import { ConsoleOperatorConfigModel } from '@console/internal/models';
import { k8sPatch, K8sResourceKind } from '@console/internal/module/k8s';
Expand Down Expand Up @@ -48,19 +48,17 @@ export const ConsolePluginModal = (props: ConsolePluginModalProps) => {
'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.',
)}
</p>
<Form>
<ConsolePluginRadioInputs
autofocus
name={pluginName}
enabled={enabled}
onChange={setEnabled}
/>
<ConsolePluginWarning
previouslyEnabled={previouslyEnabled}
enabled={enabled}
trusted={trusted}
/>
</Form>
<ConsolePluginRadioInputs
autofocus
name={pluginName}
enabled={enabled}
onChange={setEnabled}
/>
<ConsolePluginWarning
previouslyEnabled={previouslyEnabled}
enabled={enabled}
trusted={trusted}
/>
</ModalBody>
<ModalSubmitFooter
errorMessage={errorMessage}
Expand All @@ -73,7 +71,29 @@ export const ConsolePluginModal = (props: ConsolePluginModalProps) => {
);
};

export const consolePluginModal = createModalLauncher(ConsolePluginModal);
type ConsolePluginModalProviderProps = {
consoleOperatorConfig: K8sResourceKind;
csvPluginsCount?: number;
pluginName: string;
trusted: boolean;
};

const ConsolePluginModalProvider: OverlayComponent<ConsolePluginModalProviderProps> = (props) => {
return (
<ModalWrapper blocking onClose={props.closeOverlay}>
<ConsolePluginModal
close={props.closeOverlay}
cancel={props.closeOverlay}
consoleOperatorConfig={props.consoleOperatorConfig}
csvPluginsCount={props.csvPluginsCount}
pluginName={props.pluginName}
trusted={props.trusted}
/>
</ModalWrapper>
);
};

export default ConsolePluginModalProvider;

export type ConsolePluginModalProps = {
consoleOperatorConfig: K8sResourceKind;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const consolePluginModal = (props) =>
import('./ConsolePluginModal' /* webpackChunkName: "shared-modals" */).then((m) =>
m.consolePluginModal(props),
m.default(props),
);

export const deleteResourceModal = (props) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -77,7 +78,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 ConsolePluginModalProvider from '@console/shared/src/components/modals/ConsolePluginModal';
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';
Expand Down Expand Up @@ -239,6 +240,7 @@ const ManagedNamespaces: FC<ManagedNamespacesProps> = ({ obj }) => {
};

const ConsolePlugins: FC<ConsolePluginsProps> = ({ csvPlugins, trusted }) => {
const launchOverlay = useOverlay();
const console: WatchK8sResource = {
kind: referenceForModel(ConsoleOperatorConfigModel),
isList: false,
Expand Down Expand Up @@ -273,7 +275,7 @@ const ConsolePlugins: FC<ConsolePluginsProps> = ({ csvPlugins, trusted }) => {
type="button"
isInline
onClick={() =>
consolePluginModal({
launchOverlay(ConsolePluginModalProvider, {
consoleOperatorConfig,
pluginName,
trusted,
Expand Down