From 8bd7e8187dce89ecde0d886ab22361f44a746224 Mon Sep 17 00:00:00 2001 From: Nick De Villiers Date: Fri, 13 Dec 2024 08:04:12 +0000 Subject: [PATCH] feat(machines): Add 'Power cycle' as a feature-flagged action MAASENG-3945 (#5566) - Added `VITE_APP_DPU_PROVISIONING` feature flag to env (default: `false`) - Added "Power cycle" to power action menu - Refactored `getFormComponent` in `MachineActionFormWrapper` to an object of key-value pairs to reduce complexity - (drive-by) Replaced `for` loop test for valid ports with a handful of explicit test cases in `src/app/utils/isValidPortNumber.test.ts` Resolves [MAASENG-3945](https://warthogs.atlassian.net/browse/MAASENG-3945) --- .env | 6 +- .../NodeActionMenu/NodeActionMenu.tsx | 10 + .../NodeActionMenuGroup.tsx | 10 + .../MachineActionFormWrapper.tsx | 232 +++++++++++------- src/app/machines/constants.ts | 1 + src/app/store/machine/slice.ts | 5 + src/app/store/types/node.ts | 1 + src/app/store/utils/node/base.ts | 9 +- src/app/utils/isValidPortNumber.test.ts | 10 +- 9 files changed, 187 insertions(+), 97 deletions(-) diff --git a/.env b/.env index e3f26d2c4e..c5ee254d0f 100644 --- a/.env +++ b/.env @@ -6,4 +6,8 @@ VITE_BASENAME="/r" VITE_APP_BASENAME=${BASENAME} VITE_APP_VITE_BASENAME=${VITE_BASENAME} VITE_APP_WEBSOCKET_DEBUG=false -VITE_APP_USABILLA_ID=fd6cf482fbbb \ No newline at end of file +VITE_APP_USABILLA_ID=fd6cf482fbbb + +# Feature flags + +VITE_APP_DPU_PROVISIONING=false \ No newline at end of file diff --git a/src/app/base/components/NodeActionMenu/NodeActionMenu.tsx b/src/app/base/components/NodeActionMenu/NodeActionMenu.tsx index aa39280fcf..4216f18d85 100644 --- a/src/app/base/components/NodeActionMenu/NodeActionMenu.tsx +++ b/src/app/base/components/NodeActionMenu/NodeActionMenu.tsx @@ -105,6 +105,16 @@ const getTakeActionLinks = ( if (excludeActions.includes(action)) { return groupLinks; } + + // Only show "Power cycle" if the feature flag is enabled. + // TODO: Remove DPU provisioning feature flag https://warthogs.atlassian.net/browse/MAASENG-4186 + if ( + action === NodeActions.POWER_CYCLE && + import.meta.env.VITE_APP_DPU_PROVISIONING !== "true" + ) { + return groupLinks; + } + // When nodes are not provided then counts should not be visible. const count = nodes?.reduce( diff --git a/src/app/base/components/NodeActionMenuGroup/NodeActionMenuGroup.tsx b/src/app/base/components/NodeActionMenuGroup/NodeActionMenuGroup.tsx index 960df88e22..75f337f36a 100644 --- a/src/app/base/components/NodeActionMenuGroup/NodeActionMenuGroup.tsx +++ b/src/app/base/components/NodeActionMenuGroup/NodeActionMenuGroup.tsx @@ -65,6 +65,7 @@ const actionGroups: ActionGroup[] = [ actions: [ NodeActions.ON, NodeActions.OFF, + NodeActions.POWER_CYCLE, NodeActions.SOFT_OFF, NodeActions.CHECK_POWER, ], @@ -127,6 +128,15 @@ const generateActionMenus = ( return groupLinks; } + // Only show "Power cycle" if the feature flag is enabled. + // TODO: Remove DPU provisioning feature flag https://warthogs.atlassian.net/browse/MAASENG-4186 + if ( + action === NodeActions.POWER_CYCLE && + import.meta.env.VITE_APP_DPU_PROVISIONING !== "true" + ) { + return groupLinks; + } + if ( singleNode && (action === NodeActions.LOCK || action === NodeActions.UNLOCK) diff --git a/src/app/machines/components/MachineForms/MachineActionFormWrapper/MachineActionFormWrapper.tsx b/src/app/machines/components/MachineForms/MachineActionFormWrapper/MachineActionFormWrapper.tsx index 1286021af9..b4fe919542 100644 --- a/src/app/machines/components/MachineForms/MachineActionFormWrapper/MachineActionFormWrapper.tsx +++ b/src/app/machines/components/MachineForms/MachineActionFormWrapper/MachineActionFormWrapper.tsx @@ -99,97 +99,147 @@ export const MachineActionForm = ({ if (!filter) { return null; } - switch (action) { - case NodeActions.CLONE: - return ( - - ); - case NodeActions.COMMISSION: - return ; - case NodeActions.DELETE: - return ( - { - dispatchForSelectedMachines(machineActions.delete); - }} - redirectURL={urls.machines.index} - {...commonNodeFormProps} - /> - ); - case NodeActions.DEPLOY: - return ; - case NodeActions.MARK_BROKEN: - return ; - case NodeActions.OVERRIDE_FAILED_TESTING: - return ; - case NodeActions.RELEASE: - return ; - case NodeActions.SET_POOL: - return ; - case NodeActions.SET_ZONE: - return ( - - onSubmit={(zoneID) => { - dispatch(machineActions.cleanup()); - dispatchForSelectedMachines(machineActions.setZone, { - zone_id: zoneID, - }); - }} - {...commonNodeFormProps} - /> - ); - case NodeActions.TAG: - case NodeActions.UNTAG: - return ; - case NodeActions.TEST: - return ( - - applyConfiguredNetworking={applyConfiguredNetworking} - hardwareType={hardwareType} - onTest={(args) => { - dispatchForSelectedMachines(machineActions.test, { - enable_ssh: args.enableSSH, - script_input: args.scriptInputs, - testing_scripts: args.scripts.map((script) => script.name), - }); - }} - {...commonNodeFormProps} - /> - ); - case NodeActions.ABORT: - case NodeActions.ACQUIRE: - case NodeActions.EXIT_RESCUE_MODE: - case NodeActions.LOCK: - case NodeActions.MARK_FIXED: - case NodeActions.ON: - case NodeActions.RESCUE_MODE: - case NodeActions.UNLOCK: - return ( - - ); - case NodeActions.OFF: - case NodeActions.SOFT_OFF: - return ( - - ); - // No form should be opened for this, as it should only - // be available for machine details, and will be dispatched - // immediately on click. - case NodeActions.CHECK_POWER: - return null; - } + + return formComponents[action](); + }; + + const formComponents = { + [NodeActions.CLONE]: () => ( + + ), + [NodeActions.COMMISSION]: () => ( + + ), + [NodeActions.DELETE]: () => ( + { + dispatchForSelectedMachines(machineActions.delete); + }} + redirectURL={urls.machines.index} + {...commonNodeFormProps} + /> + ), + [NodeActions.DEPLOY]: () => , + [NodeActions.MARK_BROKEN]: () => ( + + ), + [NodeActions.OVERRIDE_FAILED_TESTING]: () => ( + + ), + [NodeActions.RELEASE]: () => , + [NodeActions.SET_POOL]: () => , + [NodeActions.SET_ZONE]: () => ( + + onSubmit={(zoneID) => { + dispatch(machineActions.cleanup()); + dispatchForSelectedMachines(machineActions.setZone, { + zone_id: zoneID, + }); + }} + {...commonNodeFormProps} + /> + ), + [NodeActions.TAG]: () => , + [NodeActions.UNTAG]: () => , + [NodeActions.TEST]: () => ( + + applyConfiguredNetworking={applyConfiguredNetworking} + hardwareType={hardwareType} + onTest={(args) => { + dispatchForSelectedMachines(machineActions.test, { + enable_ssh: args.enableSSH, + script_input: args.scriptInputs, + testing_scripts: args.scripts.map((script) => script.name), + }); + }} + {...commonNodeFormProps} + /> + ), + [NodeActions.ABORT]: () => ( + + ), + [NodeActions.ACQUIRE]: () => ( + + ), + [NodeActions.EXIT_RESCUE_MODE]: () => ( + + ), + [NodeActions.LOCK]: () => ( + + ), + [NodeActions.MARK_FIXED]: () => ( + + ), + [NodeActions.ON]: () => ( + + ), + [NodeActions.RESCUE_MODE]: () => ( + + ), + [NodeActions.UNLOCK]: () => ( + + ), + [NodeActions.POWER_CYCLE]: () => ( + + ), + [NodeActions.OFF]: () => ( + + ), + [NodeActions.SOFT_OFF]: () => ( + + ), + // No form should be opened for this, as it should only + // be available for machine details, and will be dispatched + // immediately on click. + [NodeActions.CHECK_POWER]: () => null, }; if (selectedCountLoading) { diff --git a/src/app/machines/constants.ts b/src/app/machines/constants.ts index 8719fd00f8..fe843befcd 100644 --- a/src/app/machines/constants.ts +++ b/src/app/machines/constants.ts @@ -17,6 +17,7 @@ export const MachineActionSidePanelViews = { POWER_OFF_MACHINE: ["machineActionForm", NodeActions.OFF], POWER_OFF_MACHINE_SOFT: ["machineActionForm", NodeActions.SOFT_OFF], POWER_ON_MACHINE: ["machineActionForm", NodeActions.ON], + POWER_CYCLE_MACHINE: ["machineActionForm", NodeActions.POWER_CYCLE], OVERRIDE_FAILED_TESTING_MACHINE: [ "machineActionForm", NodeActions.OVERRIDE_FAILED_TESTING, diff --git a/src/app/store/machine/slice.ts b/src/app/store/machine/slice.ts index 3918f4ea49..522068b5cc 100644 --- a/src/app/store/machine/slice.ts +++ b/src/app/store/machine/slice.ts @@ -1360,6 +1360,11 @@ const machineSlice = createSlice({ [`${NodeActions.RELEASE}Error`]: statusHandlers.release.error, [`${NodeActions.RELEASE}Start`]: statusHandlers.release.start, [`${NodeActions.RELEASE}Success`]: statusHandlers.release.success, + // TODO: Add actual functionality here once the backend is ready https://warthogs.atlassian.net/browse/MAASENG-4185 + powerCycle: { + prepare: () => ({ payload: null }), + reducer: () => {}, + }, removeRequest: { prepare: (callId: string) => ({ diff --git a/src/app/store/types/node.ts b/src/app/store/types/node.ts index a274c7c1b4..efff2a0aa2 100644 --- a/src/app/store/types/node.ts +++ b/src/app/store/types/node.ts @@ -179,6 +179,7 @@ export enum NodeActions { OFF = "off", ON = "on", OVERRIDE_FAILED_TESTING = "override-failed-testing", + POWER_CYCLE = "power-cycle", // TODO: Verify string with backend https://warthogs.atlassian.net/browse/MAASENG-4185 RELEASE = "release", RESCUE_MODE = "rescue-mode", SET_POOL = "set-pool", diff --git a/src/app/store/utils/node/base.ts b/src/app/store/utils/node/base.ts index 7bd156c276..9bc4df85d9 100644 --- a/src/app/store/utils/node/base.ts +++ b/src/app/store/utils/node/base.ts @@ -63,15 +63,16 @@ export const getNodeActionTitle = (actionName: NodeActions): string => { [NodeActions.MARK_FIXED]: "Mark fixed", [NodeActions.OFF]: "Power off", [NodeActions.ON]: "Power on", - [NodeActions.SOFT_OFF]: "Soft power off", [NodeActions.OVERRIDE_FAILED_TESTING]: "Override failed testing", + [NodeActions.POWER_CYCLE]: "Power cycle", [NodeActions.RELEASE]: "Release", [NodeActions.RESCUE_MODE]: "Enter rescue mode", [NodeActions.SET_POOL]: "Set pool", [NodeActions.SET_ZONE]: "Set zone", + [NodeActions.SOFT_OFF]: "Soft power off", [NodeActions.TAG]: "Tag", - [NodeActions.UNTAG]: "Untag", [NodeActions.TEST]: "Test", + [NodeActions.UNTAG]: "Untag", [NodeActions.UNLOCK]: "Unlock", }; @@ -128,6 +129,10 @@ export const getNodeActionLabel = ( `Override failed tests for ${modelString}`, `Overriding failed tests for ${modelString}`, ], + [NodeActions.POWER_CYCLE]: [ + `Power cycle ${modelString}`, + `Power cycling ${modelString}`, + ], [NodeActions.RELEASE]: [ `Release ${modelString}`, `Releasing ${modelString}`, diff --git a/src/app/utils/isValidPortNumber.test.ts b/src/app/utils/isValidPortNumber.test.ts index c311d856b8..4303d6f680 100644 --- a/src/app/utils/isValidPortNumber.test.ts +++ b/src/app/utils/isValidPortNumber.test.ts @@ -1,9 +1,13 @@ import { isValidPortNumber } from "."; it("returns true for any number between 0 and 65535", () => { - for (let i = 0; i <= 65535; i++) { - expect(isValidPortNumber(i)).toBe(true); - } + expect(isValidPortNumber(0)).toBe(true); + expect(isValidPortNumber(80)).toBe(true); + expect(isValidPortNumber(443)).toBe(true); + expect(isValidPortNumber(1234)).toBe(true); + expect(isValidPortNumber(5240)).toBe(true); + expect(isValidPortNumber(8400)).toBe(true); + expect(isValidPortNumber(65535)).toBe(true); }); it("returns false for numbers larger than 65535", () => {