diff --git a/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/ActionFormWrapper.js b/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/ActionFormWrapper.js index f9f7ad1e81..2913026ce5 100644 --- a/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/ActionFormWrapper.js +++ b/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/ActionFormWrapper.js @@ -8,6 +8,7 @@ import { machine as machineActions } from "app/base/actions"; import { machine as machineSelectors } from "app/base/selectors"; import ActionForm from "./ActionForm"; import DeployForm from "./DeployForm"; +import SetPoolForm from "./SetPoolForm"; import SetZoneForm from "./SetZoneForm"; const getErrorSentence = (action, count) => { @@ -61,6 +62,8 @@ export const ActionFormWrapper = ({ selectedAction, setSelectedAction }) => { switch (selectedAction.name) { case "deploy": return ; + case "set-pool": + return ; case "set-zone": return ; default: diff --git a/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/SetPoolForm.js b/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/SetPoolForm.js new file mode 100644 index 0000000000..7b2d4b6d2f --- /dev/null +++ b/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/SetPoolForm.js @@ -0,0 +1,73 @@ +import { useDispatch, useSelector } from "react-redux"; +import * as Yup from "yup"; +import pluralize from "pluralize"; +import React from "react"; + +import { + machine as machineActions, + resourcepool as resourcePoolActions, +} from "app/base/actions"; +import { + machine as machineSelectors, + resourcepool as resourcePoolSelectors, +} from "app/base/selectors"; +import FormikForm from "app/base/components/FormikForm"; +import FormCardButtons from "app/base/components/FormCardButtons"; +import SetPoolFormFields from "./SetPoolFormFields"; + +const SetPoolSchema = Yup.object().shape({ + description: Yup.string(), + name: Yup.string().required("Resource pool required"), + poolSelection: Yup.string().oneOf(["create", "select"]).required(), +}); + +export const SetPoolForm = ({ setSelectedAction }) => { + const dispatch = useDispatch(); + + const selectedMachines = useSelector(machineSelectors.selected); + const saved = useSelector(machineSelectors.saved); + const saving = useSelector(machineSelectors.saving); + const errors = useSelector(machineSelectors.errors); + const resourcePools = useSelector(resourcePoolSelectors.all); + + return ( + setSelectedAction(null)} + onSaveAnalytics={{ + action: "Set resource pool", + category: "Take action menu", + label: "Set resource pool of selected machines", + }} + onSubmit={(values) => { + if (values.poolSelection === "create") { + // TODO: Add method for creating a pool then setting selected machines to it + // https://github.com/canonical-web-and-design/maas-ui/issues/928 + dispatch(resourcePoolActions.create(values)); + } + const pool = resourcePools.find((pool) => pool.name === values.name); + if (pool) { + selectedMachines.forEach((machine) => { + dispatch(machineActions.setPool(machine.system_id, pool.id)); + }); + } + setSelectedAction(null); + }} + saving={saving} + saved={saved} + validationSchema={SetPoolSchema} + > + + + ); +}; + +export default SetPoolForm; diff --git a/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/SetPoolForm.test.js b/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/SetPoolForm.test.js new file mode 100644 index 0000000000..636f6bf830 --- /dev/null +++ b/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/SetPoolForm.test.js @@ -0,0 +1,94 @@ +import { act } from "react-dom/test-utils"; +import { MemoryRouter } from "react-router-dom"; +import { mount } from "enzyme"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; +import React from "react"; + +import SetPoolForm from "./SetPoolForm"; + +const mockStore = configureStore(); + +describe("SetPoolForm", () => { + let state; + beforeEach(() => { + state = { + machine: { + errors: {}, + loading: false, + loaded: true, + items: [ + { + system_id: "abc123", + }, + { + system_id: "def456", + }, + ], + selected: [], + }, + resourcepool: { + items: [ + { id: 0, name: "default" }, + { id: 1, name: "pool-1" }, + ], + }, + }; + }); + + it("correctly dispatches actions to set pools of selected machines", () => { + const store = mockStore(state); + state.machine.selected = ["abc123", "def456"]; + const wrapper = mount( + + + + + + ); + + act(() => + wrapper.find("Formik").props().onSubmit({ + poolSelection: "select", + name: "pool-1", + description: "", + }) + ); + expect(store.getActions()).toStrictEqual([ + { + type: "SET_MACHINE_POOL", + meta: { + model: "machine", + method: "action", + }, + payload: { + params: { + action: "set-pool", + extra: { + pool_id: 1, + }, + system_id: "abc123", + }, + }, + }, + { + type: "SET_MACHINE_POOL", + meta: { + model: "machine", + method: "action", + }, + payload: { + params: { + action: "set-pool", + extra: { + pool_id: 1, + }, + system_id: "def456", + }, + }, + }, + ]); + }); +}); diff --git a/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/SetPoolFormFields/SetPoolFormFields.js b/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/SetPoolFormFields/SetPoolFormFields.js new file mode 100644 index 0000000000..b1bbc19792 --- /dev/null +++ b/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/SetPoolFormFields/SetPoolFormFields.js @@ -0,0 +1,68 @@ +import { Col, Row, Select } from "@canonical/react-components"; +import { useFormikContext } from "formik"; +import { useSelector } from "react-redux"; +import React from "react"; + +import { resourcepool as resourcePoolSelectors } from "app/base/selectors"; + +import FormikField from "app/base/components/FormikField"; + +export const SetPoolFormFields = () => { + const resourcePools = useSelector(resourcePoolSelectors.all); + const { values } = useFormikContext(); + + const resourcePoolOptions = [ + { label: "Select resource pool", value: "", disabled: true }, + ...resourcePools.map((pool) => ({ + key: `pool-${pool.id}`, + label: pool.name, + value: pool.name, + })), + ]; + + return ( + + +
    +
  • + +
  • +
  • + {/* Disabled until we have a method for handling sequenced websocket requests. + https://github.com/canonical-web-and-design/maas-ui/issues/928 */} + +
  • +
+ {values.poolSelection === "select" ? ( + + ) : ( + <> + + + + )} + +
+ ); +}; + +export default SetPoolFormFields; diff --git a/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/SetPoolFormFields/SetPoolFormFields.test.js b/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/SetPoolFormFields/SetPoolFormFields.test.js new file mode 100644 index 0000000000..e5137a59d4 --- /dev/null +++ b/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/SetPoolFormFields/SetPoolFormFields.test.js @@ -0,0 +1,79 @@ +import { act } from "react-dom/test-utils"; +import { MemoryRouter } from "react-router-dom"; +import { mount } from "enzyme"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; +import React from "react"; + +import SetPoolForm from "../SetPoolForm"; + +const mockStore = configureStore(); + +describe("SetPoolFormFields", () => { + let state; + beforeEach(() => { + state = { + machine: { + errors: {}, + loading: false, + loaded: true, + items: [ + { + system_id: "abc123", + }, + { + system_id: "def456", + }, + ], + selected: ["abc123", "def456"], + }, + resourcepool: { + items: [ + { id: 0, name: "default" }, + { id: 1, name: "pool-1" }, + ], + }, + }; + }); + + it("shows a select if select pool radio chosen", async () => { + const store = mockStore(state); + const wrapper = mount( + + + + + + ); + await act(async () => { + wrapper.find("[data-test='select-pool'] input").simulate("change", { + target: { name: "poolSelection", value: "select" }, + }); + }); + wrapper.update(); + expect(wrapper.find("Select").exists()).toBe(true); + }); + + it.skip("shows inputs for creating a pool if create pool radio chosen", async () => { + const store = mockStore(state); + const wrapper = mount( + + + + + + ); + await act(async () => { + wrapper.find("[data-test='create-pool'] input").simulate("change", { + target: { name: "poolSelection", value: "create" }, + }); + }); + wrapper.update(); + expect(wrapper.find("Input[name='name']").exists()).toBe(true); + expect(wrapper.find("Input[name='description']").exists()).toBe(true); + }); +}); diff --git a/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/SetPoolFormFields/index.js b/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/SetPoolFormFields/index.js new file mode 100644 index 0000000000..f5eaa3f47b --- /dev/null +++ b/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/SetPoolFormFields/index.js @@ -0,0 +1 @@ +export { default } from "./SetPoolFormFields"; diff --git a/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/index.js b/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/index.js new file mode 100644 index 0000000000..dd63228c16 --- /dev/null +++ b/ui/src/app/machines/components/HeaderStrip/ActionFormWrapper/SetPoolForm/index.js @@ -0,0 +1 @@ +export { default } from "./SetPoolForm";