Skip to content

Commit

Permalink
Create a pool and attach machines to it.
Browse files Browse the repository at this point in the history
  • Loading branch information
huwshimi committed Apr 16, 2020
1 parent f268280 commit 74bae5d
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 28 deletions.
15 changes: 15 additions & 0 deletions ui/src/app/base/actions/resourcepool/resourcepool.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,19 @@ import { createStandardActions } from "app/utils/redux";

const resourcepool = createStandardActions("resourcepool");

/**
* Create a pool and attach machines to it.
* @param {Object} pool - The pool details.
* @param {Array} machines - A list of machine ids.
*/
resourcepool.createWithMachines = (pool, machines) => ({
type: "CREATE_RESOURCEPOOL_WITH_MACHINES",
payload: {
params: {
pool,
machines,
},
},
});

export default resourcepool;
19 changes: 19 additions & 0 deletions ui/src/app/base/actions/resourcepool/resourcepool.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,23 @@ describe("resourcepool actions", () => {
type: "CLEANUP_RESOURCEPOOL",
});
});

it("can handle creating resource pools with machines", () => {
expect(
resourcepool.createWithMachines({ name: "pool1" }, [
"machine1",
"machine2",
])
).toEqual({
type: "CREATE_RESOURCEPOOL_WITH_MACHINES",
payload: {
params: {
pool: {
name: "pool1",
},
machines: ["machine1", "machine2"],
},
},
});
});
});
47 changes: 47 additions & 0 deletions ui/src/app/base/sagas/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { call } from "redux-saga/effects";

import {
machine as machineActions,
resourcepool as resourcePoolActions,
} from "app/base/actions";

/**
* Generate functions that will use the response to create the dispatchable
* action to set the pool for each machine.
* @param {Array} machines - A list of machine ids.
* @returns {Array} The list of action creator functions.
*/
export const generateActionCreators = (machines) =>
machines.map((machineID) => (result) =>
machineActions.setPool(machineID, result.id)
);

/**
* Handle creating a pool and then attaching machines to that pool.
* @param {Object} socketClient - The websocket client instance.
* @param {Function} sendMessage - The saga that handles sending websocket messages.
* @param {Object} action - The redux action with pool and machine data.
*/
export function* createPoolWithMachines(
socketClient,
sendMessage,
{ payload }
) {
const { machines, pool } = payload.params;
const actionCreators = yield call(generateActionCreators, machines);
// Send the initial action via the websocket.
yield call(
sendMessage,
socketClient,
resourcePoolActions.create(pool),
actionCreators
);
}

// Sagas to be handled by the websocket channel.
export default [
{
action: "CREATE_RESOURCEPOOL_WITH_MACHINES",
method: createPoolWithMachines,
},
];
34 changes: 34 additions & 0 deletions ui/src/app/base/sagas/actions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { expectSaga } from "redux-saga-test-plan";
import * as matchers from "redux-saga-test-plan/matchers";

import { createPoolWithMachines, generateActionCreators } from "./actions";

jest.mock("../../../websocket-client");

describe("websocket sagas", () => {
it("can send a message to create a pool then attach machines", () => {
const socketClient = jest.fn();
const sendMessage = jest.fn();
const actionCreators = [jest.fn()];
const pool = { name: "pool1", description: "a pool" };
const action = { payload: { params: { machines: ["machine1"], pool } } };
return expectSaga(createPoolWithMachines, socketClient, sendMessage, action)
.provide([[matchers.call.fn(generateActionCreators), actionCreators]])
.call(
sendMessage,
socketClient,
{
type: "CREATE_RESOURCEPOOL",
payload: {
params: pool,
},
meta: {
model: "resourcepool",
method: "create",
},
},
actionCreators
)
.run();
});
});
2 changes: 2 additions & 0 deletions ui/src/app/base/sagas/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import actionHandlers from "./actions";
import { watchWebSockets } from "./websockets";
import {
watchCheckAuthenticated,
Expand All @@ -15,6 +16,7 @@ import {
} from "./http";

export {
actionHandlers,
watchCheckAuthenticated,
watchLogin,
watchLogout,
Expand Down
88 changes: 73 additions & 15 deletions ui/src/app/base/sagas/websockets.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ import WebSocketClient from "../../../websocket-client";

let loadedModels = [];

// A map of request ids to action creators. This is used to dispatch actions
// when a response is received.
const nextActions = new Map();
export const getNextActions = (id) => nextActions.get(id);
export const setNextActions = (id, actionCreator) =>
nextActions.set(id, actionCreator);
export const deleteNextActions = (id) => {
nextActions.delete(id);
};

// A store of websocket requests that need to be called to fetch the next batch
// of data. The map is between request id and redux action object.
const batchRequests = new Map();
Expand Down Expand Up @@ -169,6 +179,40 @@ export function* handleBatch({ request_id, result }) {
}
}

/**
* Store the actions to dispatch when the response is received.
*
* @param {Object} action - A Redux action.
* @param {Array} requestIDs - A list of ids for the requests associated with
* this action.
*/
function* storeNextActions(nextActionCreators, requestIDs) {
if (nextActionCreators) {
for (let id of requestIDs) {
yield call(setNextActions, id, nextActionCreators);
}
}
}

/**
* Handle dispatching the next actions, if required.
*
* @param {Object} response - A websocket response.
*/
export function* handleNextActions({ request_id, result }) {
const actionCreators = yield call(getNextActions, request_id);
if (actionCreators && actionCreators.length) {
for (let actionCreator of actionCreators) {
// Generate the action object using the result from the response.
const action = yield call(actionCreator, result);
// Dispatch the action.
yield put(action);
}
// Clean up the stored action creators.
yield call(deleteNextActions, request_id);
}
}

/**
* Handle messages received over the WebSocket.
*/
Expand Down Expand Up @@ -208,6 +252,8 @@ export function* handleMessage(socketChannel, socketClient) {
yield put({ type: `${action_type}_SUCCESS`, payload: response.result });
// Handle batching, if required.
yield call(handleBatch, response);
// Handle dispatching next actions, if required.
yield call(handleNextActions, response);
}
}
}
Expand Down Expand Up @@ -240,7 +286,7 @@ const buildMessage = (meta, params) => {
/**
* Send WebSocket messages via the client.
*/
export function* sendMessage(socketClient, action) {
export function* sendMessage(socketClient, action, nextActionCreators) {
const { meta, payload, type } = action;
let params = payload ? payload.params : null;
const { method, model } = meta;
Expand Down Expand Up @@ -282,8 +328,10 @@ export function* sendMessage(socketClient, action) {
);
requestIDs.push(id);
}
// Store the actions to dispatch when the response is received.
yield call(storeNextActions, nextActionCreators, requestIDs);
// Queue batching, if required.
queueBatch(action, requestIDs);
yield call(queueBatch, action, requestIDs);
} catch (error) {
yield put({ type: `${type}_ERROR`, error });
}
Expand All @@ -292,7 +340,7 @@ export function* sendMessage(socketClient, action) {
/**
* Connect to the WebSocket and watch for message.
*/
export function* setupWebSocket() {
export function* setupWebSocket(messageHandlers = []) {
try {
const csrftoken = yield call(getCookie, "csrftoken");
if (!csrftoken) {
Expand All @@ -307,16 +355,24 @@ export function* setupWebSocket() {
const socketChannel = yield call(watchMessages, socketClient);
while (true) {
let { cancel } = yield race({
task: all([
call(handleMessage, socketChannel, socketClient),
// Using takeEvery() instead of call() here to get around this issue:
// https://github.com/canonical-web-and-design/maas-ui/issues/172
takeEvery(
(action) => isWebsocketRequestAction(action),
sendMessage,
socketClient
),
]),
task: all(
[
call(handleMessage, socketChannel, socketClient),
// Using takeEvery() instead of call() here to get around this issue:
// https://github.com/canonical-web-and-design/maas-ui/issues/172
takeEvery(
(action) => isWebsocketRequestAction(action),
sendMessage,
socketClient
),
].concat(
// Attach the additional actions that should be taken by the
// websocket channel.
messageHandlers.map(({ action, method }) =>
takeEvery(action, method, socketClient, sendMessage)
)
)
),
cancel: take("WEBSOCKET_DISCONNECT"),
});
if (cancel) {
Expand All @@ -331,7 +387,9 @@ export function* setupWebSocket() {

/**
* Set up websocket connections when requested.
* @param {Array} messageHandlers - Additional sagas to be handled by the
* websocket channel.
*/
export function* watchWebSockets() {
yield takeLatest("WEBSOCKET_CONNECT", setupWebSocket);
export function* watchWebSockets(messageHandlers) {
yield takeLatest("WEBSOCKET_CONNECT", setupWebSocket, messageHandlers);
}
37 changes: 37 additions & 0 deletions ui/src/app/base/sagas/websockets.test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { call, put, take } from "redux-saga/effects";
import { expectSaga } from "redux-saga-test-plan";
import * as matchers from "redux-saga-test-plan/matchers";

import MESSAGE_TYPES from "app/base/constants";
import {
createConnection,
getBatchRequest,
getNextActions,
handleBatch,
handleMessage,
handleNextActions,
handleNotifyMessage,
sendMessage,
setNextActions,
watchMessages,
watchWebSockets,
} from "./websockets";
Expand Down Expand Up @@ -109,6 +113,25 @@ describe("websocket sagas", () => {
);
});

it("can store a next action when sending a WebSocket message", () => {
const action = {
type: "TEST_ACTION",
meta: {
model: "test",
method: "method",
type: MESSAGE_TYPES.REQUEST,
},
payload: {
params: { foo: "bar" },
},
};
const nextActionCreators = [jest.fn()];
return expectSaga(sendMessage, socketClient, action, nextActionCreators)
.provide([[matchers.call.fn(socketClient.send), 808]])
.call(setNextActions, 808, nextActionCreators)
.run();
});

it("continues if data has already been fetched for list methods", () => {
const action = {
type: "FETCH_TEST",
Expand Down Expand Up @@ -284,6 +307,20 @@ describe("websocket sagas", () => {
.run();
});

it("can dispatch a next action", () => {
const response = {
request_id: 99,
result: { id: 808 },
};
const action = { type: "NEXT_ACTION" };
const actionCreator = jest.fn(() => action);
return expectSaga(handleNextActions, response)
.provide([[call(getNextActions, 99), [actionCreator]]])
.call(actionCreator, response.result)
.put(action)
.run();
});

it("can handle a WebSocket error message", () => {
const saga = handleMessage(socketChannel, socketClient);
expect(saga.next().value).toEqual(take(socketChannel));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ export const SetPoolForm = ({ setSelectedAction }) => {
}}
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));
});
const machineIDs = selectedMachines.map(({ system_id }) => system_id);
dispatch(resourcePoolActions.createWithMachines(values, machineIDs));
} else {
const pool = resourcePools.find((pool) => pool.name === values.name);
if (pool) {
selectedMachines.forEach((machine) => {
dispatch(machineActions.setPool(machine.system_id, pool.id));
});
}
}
setSelectedAction(null);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,8 @@ export const SetPoolFormFields = () => {
/>
</li>
<li className="p-inline-list__item">
{/* Disabled until we have a method for handling sequenced websocket requests.
https://github.com/canonical-web-and-design/maas-ui/issues/928 */}
<FormikField
data-test="create-pool"
disabled
label="Create pool"
name="poolSelection"
type="radio"
Expand Down
Loading

0 comments on commit 74bae5d

Please sign in to comment.