WIP on redux refactor
This commit is contained in:
parent
1ec0083abb
commit
636ade0273
7 changed files with 677 additions and 10 deletions
|
@ -17,10 +17,13 @@
|
||||||
"react-circular-slider-svg": "^0.1.5",
|
"react-circular-slider-svg": "^0.1.5",
|
||||||
"react-device-detect": "^1.11.14",
|
"react-device-detect": "^1.11.14",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
|
"react-redux": "^7.2.0",
|
||||||
"react-round-slider": "^1.0.1",
|
"react-round-slider": "^1.0.1",
|
||||||
"react-router": "^5.1.2",
|
"react-router": "^5.1.2",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "3.4.0",
|
"react-scripts": "3.4.0",
|
||||||
|
"redux": "^4.0.5",
|
||||||
|
"redux-thunk": "^2.3.0",
|
||||||
"semantic-ui-react": "^0.88.2",
|
"semantic-ui-react": "^0.88.2",
|
||||||
"styled-components": "^5.0.1"
|
"styled-components": "^5.0.1"
|
||||||
},
|
},
|
||||||
|
|
|
@ -271,12 +271,6 @@ export var call = {
|
||||||
headers: { Authorization: "Bearer " + tkn },
|
headers: { Authorization: "Bearer " + tkn },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deviceGetById: function (data, headers) {
|
|
||||||
return axios.get(config + data.device + "/" + data.id);
|
|
||||||
},
|
|
||||||
deviceGetAll: function (data, headers) {
|
|
||||||
return axios.get(config + data.device);
|
|
||||||
},
|
|
||||||
smartPlugReset: function (id) {
|
smartPlugReset: function (id) {
|
||||||
return axios.delete(config + "smartPlug/" + id + "/meter", {
|
return axios.delete(config + "smartPlug/" + id + "/meter", {
|
||||||
headers: { Authorization: "Bearer " + tkn },
|
headers: { Authorization: "Bearer " + tkn },
|
||||||
|
|
|
@ -2,10 +2,14 @@ import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import * as serviceWorker from "./serviceWorker";
|
import * as serviceWorker from "./serviceWorker";
|
||||||
//React Router
|
import { Provider } from "react-redux";
|
||||||
//import { BrowserRouter, Route, Switch } from "react-router-dom";
|
import smartHutStore from "./store";
|
||||||
|
|
||||||
const index = <App />;
|
const index = (
|
||||||
|
<Provider store={smartHutStore}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
|
||||||
ReactDOM.render(index, document.getElementById("root"));
|
ReactDOM.render(index, document.getElementById("root"));
|
||||||
serviceWorker.unregister();
|
serviceWorker.unregister();
|
||||||
|
|
459
smart-hut/src/remote.js
Normal file
459
smart-hut/src/remote.js
Normal file
|
@ -0,0 +1,459 @@
|
||||||
|
import smartHutStore from "./store";
|
||||||
|
import actions from "./storeActions";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
class Endpoint {
|
||||||
|
axiosInstance = axios.create({
|
||||||
|
baseURL: this.URL,
|
||||||
|
validateStatus: (status) => status >= 200 && status < 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the endpoint URL (SmartHut backend URL)
|
||||||
|
* @returns {String} endpoint URL
|
||||||
|
*/
|
||||||
|
static get URL() {
|
||||||
|
return window.BACKEND_URL !== "__BACKEND_URL__"
|
||||||
|
? window.BACKEND_URL
|
||||||
|
: "http://localhost:8080";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns token for current session, null if logged out
|
||||||
|
* @returns {String|null} the token
|
||||||
|
*/
|
||||||
|
static get token() {
|
||||||
|
return smartHutStore.getState().login.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs an authenticated request
|
||||||
|
* @param {get|post|put|delete} the desired method
|
||||||
|
* @param {String} route the desired route (e.g. "/rooms/1/devices")
|
||||||
|
* @param {[String]String} query query ('?') parameters (no params by default)
|
||||||
|
* @param {any} body the JSON request body
|
||||||
|
*/
|
||||||
|
static send(method, route, query = {}, body = null) {
|
||||||
|
if (!this.token) {
|
||||||
|
throw new Error("No token while performing authenticated request");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method !== "get")
|
||||||
|
return this.axiosInstance(route, {
|
||||||
|
method: method,
|
||||||
|
params: query,
|
||||||
|
data: ["put", "post"].indexOf(method) !== -1 ? body : null,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.token}`,
|
||||||
|
},
|
||||||
|
}).then((res) => {
|
||||||
|
if (!res.data) {
|
||||||
|
console.error("Response body is empty");
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs an authenticated GET request
|
||||||
|
* @param {String} route the desired route (e.g. "/rooms/1/devices")
|
||||||
|
* @param {[String]String} query query ('?') parameters (no params by default)
|
||||||
|
*/
|
||||||
|
static get(route, query) {
|
||||||
|
return this.send("get", route, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs an authenticated POST request
|
||||||
|
* @param {String} route the desired route (e.g. "/rooms/1/devices")
|
||||||
|
* @param {[String]String} query query ('?') parameters (no params by default)
|
||||||
|
* @param {any} body the JSON request body
|
||||||
|
*/
|
||||||
|
static post(route, query, body) {
|
||||||
|
return this.send("post", route, query, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs an authenticated PUT request
|
||||||
|
* @param {String} route the desired route (e.g. "/rooms/1/devices")
|
||||||
|
* @param {[String]String} query query ('?') parameters (no params by default)
|
||||||
|
* @param {any} body the JSON request body
|
||||||
|
*/
|
||||||
|
static put(route, query, body) {
|
||||||
|
return this.send("put", route, query, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs an authenticated DELETE request
|
||||||
|
* @param {get|post|put|delete} the desired method
|
||||||
|
* @param {String} route the desired route (e.g. "/rooms/1/devices")
|
||||||
|
* @param {[String]String} query query ('?') parameters (no params by default)
|
||||||
|
*/
|
||||||
|
static delete(route, query) {
|
||||||
|
return this.send("delete", route, query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an error response, returns an array of user
|
||||||
|
* friendly messages to display to the user
|
||||||
|
* @param {*} err the Axios error reponse object
|
||||||
|
* @returns {String[]} user friendly error messages
|
||||||
|
*/
|
||||||
|
function parseValidationErrors(err) {
|
||||||
|
if (
|
||||||
|
err.response &&
|
||||||
|
err.response.status === 400 &&
|
||||||
|
err.response.data &&
|
||||||
|
Array.isArray(err.response.data.errors)
|
||||||
|
) {
|
||||||
|
return [...new Set(err.response.data.errors.map((e) => e.defaultMessage))];
|
||||||
|
} else {
|
||||||
|
console.warn("Non validation error", err);
|
||||||
|
return ["Network error"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RemoteService {
|
||||||
|
/**
|
||||||
|
* Performs login
|
||||||
|
* @param {String} usernameOrEmail
|
||||||
|
* @param {String} password
|
||||||
|
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
|
||||||
|
* with user-fiendly errors as a String array
|
||||||
|
*/
|
||||||
|
static login(usernameOrEmail, password) {
|
||||||
|
return (dispatch) => {
|
||||||
|
return Endpoint.axiosInstance
|
||||||
|
.post(`${Endpoint.URL}/auth/login`, {
|
||||||
|
usernameOrEmail,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
localStorage.setItem("token", res.token);
|
||||||
|
localStorage.setItem(
|
||||||
|
"exp",
|
||||||
|
new Date().getTime() + 5 * 60 * 60 * 1000
|
||||||
|
);
|
||||||
|
dispatch(actions.loginSuccess(res.token));
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn("login error", err);
|
||||||
|
return [
|
||||||
|
err.response.status === 401
|
||||||
|
? "Wrong credentials"
|
||||||
|
: "An error occurred while logging in",
|
||||||
|
];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs logout
|
||||||
|
* @param {String} usernameOrEmail
|
||||||
|
* @param {String} password
|
||||||
|
*/
|
||||||
|
static logout() {
|
||||||
|
return (dispatch) => void dispatch(actions.logout());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches user information via REST calls, if it is logged in
|
||||||
|
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
|
||||||
|
* with user-fiendly errors as a String array
|
||||||
|
*/
|
||||||
|
static fetchUserInfo() {
|
||||||
|
return (dispatch) => {
|
||||||
|
return Endpoint.get("/auth/profile")
|
||||||
|
.then((res) => void dispatch(actions.userInfoUpdate(res.data)))
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn("Fetch user info error", err);
|
||||||
|
return ["Network error"];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all rooms that belong to this user. This call does not
|
||||||
|
* populate the devices attribute in rooms.
|
||||||
|
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
|
||||||
|
* with user-fiendly errors as a String array
|
||||||
|
*/
|
||||||
|
static fetchAllRooms() {
|
||||||
|
return (dispatch) => {
|
||||||
|
return Endpoint.get("/room")
|
||||||
|
.then((res) => void dispatch(actions.roomsUpdate(res.data)))
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Fetch all rooms error", err);
|
||||||
|
return ["Network error"];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all devices in a particular room, or fetches all devices.
|
||||||
|
* This also updates the devices attribute on values in the map rooms.
|
||||||
|
* @param {Number|null} roomId the room to which fetch devices
|
||||||
|
* from, null to fetch from all rooms
|
||||||
|
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
|
||||||
|
* with user-fiendly errors as a String array
|
||||||
|
*/
|
||||||
|
static fetchDevices(roomId = null) {
|
||||||
|
return (dispatch) => {
|
||||||
|
return Endpoint.get(roomId ? `/room/${roomId}/device` : "/device")
|
||||||
|
.then((res) => void dispatch(actions.devicesUpdate(roomId, res.data)))
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`Fetch devices roomId=${roomId} error`, err);
|
||||||
|
return ["Network error"];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates/Updates a room with the given data
|
||||||
|
* @param {String} data.name the room's name,
|
||||||
|
* @param {String} data.icon the room's icon name in SemanticUI icons
|
||||||
|
* @param {String} data.image ths room's image, as base64
|
||||||
|
* @param {Number|null} roomId the room's id if update, null for creation
|
||||||
|
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
|
||||||
|
* with user-fiendly errors as a String array
|
||||||
|
*/
|
||||||
|
static saveRoom(data, roomId = null) {
|
||||||
|
return (dispatch) => {
|
||||||
|
data = {
|
||||||
|
name: data.name,
|
||||||
|
icon: data.icon,
|
||||||
|
image: data.image,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (roomId
|
||||||
|
? Endpoint.put(`/room/${roomId}`, {}, data)
|
||||||
|
: Endpoint.post(`/room`, {}, data)
|
||||||
|
)
|
||||||
|
.then((res) => void dispatch(actions.roomSave(res.data)))
|
||||||
|
.catch(parseValidationErrors);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates/Updates a device with the given data. If
|
||||||
|
* data.id is truthy, then a update call is performed,
|
||||||
|
* otherwise a create call is performed. The update URL
|
||||||
|
* is computed based on data.kind when data.flowType =
|
||||||
|
* 'OUTPUT', otherwise the PUT "/device" endpoint
|
||||||
|
* is used for updates and the POST "/<device.kind>"
|
||||||
|
* endpoints are used for creation.
|
||||||
|
* @param {Device} data the device to update.
|
||||||
|
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
|
||||||
|
* with user-fiendly errors as a String array
|
||||||
|
*/
|
||||||
|
static updateDevice(data) {
|
||||||
|
return (dispatch) => {
|
||||||
|
let url = "/device";
|
||||||
|
if ((data.id && data.flowType === "OUTPUT") || !data.id) {
|
||||||
|
url = "/" + data.kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Endpoint[data.id ? "put" : "post"](url, {}, data)
|
||||||
|
.then((res) => void dispatch(actions.deviceUpdate(res.data)))
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn("Update device: ", data, "error: ", err);
|
||||||
|
return ["Network error"];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static _operateInput(url, getUrl, action) {
|
||||||
|
return (dispatch) => {
|
||||||
|
return Endpoint.put(url, {}, action)
|
||||||
|
.then(async (res) => {
|
||||||
|
const inputDevice = Endpoint.get(getUrl);
|
||||||
|
delete inputDevice.outputs;
|
||||||
|
dispatch(actions.deviceOperationUpdate([...res.data, inputDevice]));
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn(`${url} error`, err);
|
||||||
|
return ["Network error"];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the state of a switch, by turning it on, off or toggling it.
|
||||||
|
*
|
||||||
|
* @typedef {"ON" | "OFF" | "TOGGLE"} SwitchOperation
|
||||||
|
*
|
||||||
|
* @param {Number} switchId the switch device id
|
||||||
|
* @param {SwitchOperation} type the operation to perform on the switch
|
||||||
|
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
|
||||||
|
* with user-fiendly errors as a String array
|
||||||
|
*/
|
||||||
|
static switchOperate(switchId, type) {
|
||||||
|
return this._operateInput("/switch/operate", `/switch/${switchId}`, {
|
||||||
|
type: type.toUpperCase(),
|
||||||
|
id: switchId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns a knob dimmer to a specific amount
|
||||||
|
*
|
||||||
|
* @param {Number} dimmerId the knob dimmer id
|
||||||
|
* @param {number} intensity the absolute intensity to dim to. Must be >=0 and <= 100.
|
||||||
|
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
|
||||||
|
* with user-fiendly errors as a String array
|
||||||
|
*/
|
||||||
|
static knobDimmerDimTo(dimmerId, intensity) {
|
||||||
|
return this._operateInput("/knobDimmer/dimTo", `/knobDimmer/${dimmerId}`, {
|
||||||
|
intensity,
|
||||||
|
id: dimmerId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns a button dimmer up or down
|
||||||
|
*
|
||||||
|
* @typedef {"UP" | "DOWN"} ButtonDimmerDimType
|
||||||
|
*
|
||||||
|
* @param {Number} dimmerId the button dimmer id
|
||||||
|
* @param {ButtonDimmerDimType} dimType the type of dim to perform
|
||||||
|
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
|
||||||
|
* with user-fiendly errors as a String array
|
||||||
|
*/
|
||||||
|
static buttonDimmerDim(dimmerId, dimType) {
|
||||||
|
return this._operateInput(
|
||||||
|
"/buttonDimmer/dim",
|
||||||
|
`/buttonDimmer/${dimmerId}`,
|
||||||
|
{
|
||||||
|
dimType,
|
||||||
|
id: dimmerId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the meter on a smart plug
|
||||||
|
*
|
||||||
|
* @param {Number} smartPlugId the smart plug to reset
|
||||||
|
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
|
||||||
|
* with user-fiendly errors as a String array
|
||||||
|
*/
|
||||||
|
static smartPlugReset(smartPlugId) {
|
||||||
|
return (dispatch) => {
|
||||||
|
return Endpoint.delete(`/smartPlug/${smartPlugId}/meter`)
|
||||||
|
.then((res) => dispatch(actions.deviceOperationUpdate([res.data])))
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn(`Smartplug reset error`, err);
|
||||||
|
return ["Network error"];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a room
|
||||||
|
* @param {Number} roomId the id of the room to delete
|
||||||
|
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
|
||||||
|
* with user-fiendly errors as a String array
|
||||||
|
*/
|
||||||
|
static deleteRoom(roomId) {
|
||||||
|
return (dispatch) => {
|
||||||
|
Endpoint.delete(`/room/${roomId}`)
|
||||||
|
.then((_) => dispatch(actions.roomDelete(roomId)))
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn("Room deletion error", err);
|
||||||
|
return ["Network error"];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a device
|
||||||
|
* @param {Number} deviceId the id of the device to delete
|
||||||
|
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
|
||||||
|
* with user-fiendly errors as a String array
|
||||||
|
*/
|
||||||
|
static deleteDevice(deviceId) {
|
||||||
|
return (dispatch) => {
|
||||||
|
Endpoint.delete(`/device/${deviceId}`)
|
||||||
|
.then((_) => dispatch(actions.deviceDelete(deviceId)))
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn("Device deletion error", err);
|
||||||
|
return ["Network error"];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Forms {
|
||||||
|
/**
|
||||||
|
* Attempts to create a new user from the given data.
|
||||||
|
* This method does not update the global state,
|
||||||
|
* please check its return value.
|
||||||
|
* @param {String} data.username the chosen username
|
||||||
|
* @param {String} data.password the chosen password
|
||||||
|
* @param {String} data.email the chosen email
|
||||||
|
* @param {String} data.name the chosen full name
|
||||||
|
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
|
||||||
|
* with validation errors as a String array
|
||||||
|
*/
|
||||||
|
static submitRegistration(data) {
|
||||||
|
return Endpoint.post(
|
||||||
|
"/register",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
username: data.username,
|
||||||
|
password: data.password,
|
||||||
|
name: data.name,
|
||||||
|
email: data.email,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((_) => void 0)
|
||||||
|
.catch(parseValidationErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a request to perform a password reset.
|
||||||
|
* This method does not update the global state,
|
||||||
|
* please check its return value.
|
||||||
|
* @param {String} email the email to which perform the reset
|
||||||
|
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
|
||||||
|
* with validation errors as a String array
|
||||||
|
*/
|
||||||
|
static submitInitResetPassword(email) {
|
||||||
|
return Endpoint.post(
|
||||||
|
"/register/init-reset-password",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
usernameOrEmail: email,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((_) => void 0)
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn("Init reset password failed", err);
|
||||||
|
return ["Network error"];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the password for the actual password reset, haviug already
|
||||||
|
* performed email verification
|
||||||
|
* This method does not update the global state,
|
||||||
|
* please check its return value.
|
||||||
|
* @param {String} password the new password
|
||||||
|
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
|
||||||
|
* with validation errors as a String array
|
||||||
|
*/
|
||||||
|
static submitResetPassword(password) {
|
||||||
|
return Endpoint.post(
|
||||||
|
"/register/reset-password",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
password,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((_) => void 0)
|
||||||
|
.catch(parseValidationErrors);
|
||||||
|
}
|
||||||
|
}
|
137
smart-hut/src/store.js
Normal file
137
smart-hut/src/store.js
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
import { createStore } from "redux";
|
||||||
|
import { RemoteService } from "./remote";
|
||||||
|
import { createDispatchHook } from "react-redux";
|
||||||
|
import actions from "./storeActions";
|
||||||
|
|
||||||
|
const initialToken = localStorage.getItem("token");
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
login: {
|
||||||
|
loggedIn: false,
|
||||||
|
token: initialToken ? initialToken : null,
|
||||||
|
},
|
||||||
|
userInfo: null,
|
||||||
|
/** @type {[integer]Room} */
|
||||||
|
rooms: {},
|
||||||
|
/** @type {[integer]Device} */
|
||||||
|
devices: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
function reducer(previousState, action) {
|
||||||
|
let newState = Object.assign({}, previousState);
|
||||||
|
const createOrUpdateRoom = (room) => {
|
||||||
|
if (!(room.id in newState.rooms)) {
|
||||||
|
newState.rooms[room.id] = room;
|
||||||
|
newState.rooms[room.id].devices = new Set();
|
||||||
|
} else {
|
||||||
|
newState.rooms[room.id].name = room.name;
|
||||||
|
newState.rooms[room.id].image = room.image;
|
||||||
|
newState.rooms[room.id].icon = room.icon;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case "LOGIN_UPDATE":
|
||||||
|
newState.login = action.login;
|
||||||
|
delete newState.errors.login;
|
||||||
|
break;
|
||||||
|
case "USER_INFO_UPDATE":
|
||||||
|
newState.user = action.user;
|
||||||
|
delete newState.errors.userInfo;
|
||||||
|
break;
|
||||||
|
case "ROOMS_UPDATE":
|
||||||
|
for (const room of action.rooms) {
|
||||||
|
createOrUpdateRoom(room);
|
||||||
|
}
|
||||||
|
delete newState.errors.rooms;
|
||||||
|
break;
|
||||||
|
case "DEVICES_UPDATE":
|
||||||
|
// if room is given, delete all devices in that room
|
||||||
|
// and remove any join between that room and deleted
|
||||||
|
// devices
|
||||||
|
if (action.roomId) {
|
||||||
|
const room = newState.rooms[action.roomId];
|
||||||
|
for (const deviceId of room.devices) {
|
||||||
|
delete newState.devices[deviceId];
|
||||||
|
}
|
||||||
|
room.devices = [];
|
||||||
|
} else if (action.partial) {
|
||||||
|
// if the update is partial and caused by an operation on an input
|
||||||
|
// device (like /switch/operate), iteratively remove deleted
|
||||||
|
// devices and their join with their corresponding room.
|
||||||
|
for (const device of action.devices) {
|
||||||
|
const room = newState.rooms[newState.devices[device.id].roomId];
|
||||||
|
room.devices.delete(device.id);
|
||||||
|
delete newState.devices[device.id];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// otherwise, just delete all devices and all joins
|
||||||
|
// between rooms and devices
|
||||||
|
newState.devices = {};
|
||||||
|
for (const room of newState.rooms) {
|
||||||
|
room.devices = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const device of action.devices) {
|
||||||
|
newState.devices[device.id] = device;
|
||||||
|
|
||||||
|
if (device.roomId in newState.rooms) {
|
||||||
|
newState.rooms[device.roomId].devices.add(device.id);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"Cannot join device",
|
||||||
|
device,
|
||||||
|
`in room ${device.roomId} since that
|
||||||
|
room has not been fetched`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete newState.errors.devices;
|
||||||
|
break;
|
||||||
|
case "ROOM_SAVE":
|
||||||
|
createOrUpdateRoom(action.room);
|
||||||
|
break;
|
||||||
|
case "ROOM_DELETE":
|
||||||
|
if (!(actions.roomId in newState.rooms)) {
|
||||||
|
console.warn(`Room to delete ${actions.roomId} does not exist`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This update does not ensure the consistent update of switchId/dimmerId properties
|
||||||
|
// on output devices connected to an input device in this room. Please manually request
|
||||||
|
// all devices again if consistent update is desired
|
||||||
|
for (const id of newState.rooms[action.roomId].devices) {
|
||||||
|
delete newState.devices[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
delete newState.rooms[action.roomId];
|
||||||
|
break;
|
||||||
|
case "DEVICE_DELETE":
|
||||||
|
if (!(actions.deviceId in newState.devices)) {
|
||||||
|
console.warn(`Device to delete ${actions.deviceId} does not exist`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
newState.rooms[newState.devices[actions.deviceId].roomId].devices.delete(
|
||||||
|
actions.deviceId
|
||||||
|
);
|
||||||
|
delete newState.devices[actions.deviceId];
|
||||||
|
break;
|
||||||
|
case "LOGOUT":
|
||||||
|
newState.login = { token: null, loggedIn: false };
|
||||||
|
newState.rooms = [];
|
||||||
|
newState.devices = [];
|
||||||
|
delete newState.errors.login;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`Action type ${action.type} unknown`, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const smartHutStore = createStore(reducer, initialState);
|
||||||
|
|
||||||
|
export default smartHutStore;
|
36
smart-hut/src/storeActions.js
Normal file
36
smart-hut/src/storeActions.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
const actions = {
|
||||||
|
loginSuccess: (token) => ({
|
||||||
|
type: "LOGIN_UPDATE",
|
||||||
|
login: {
|
||||||
|
loggedIn: true,
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
logout: () => ({
|
||||||
|
type: "LOGOUT",
|
||||||
|
}),
|
||||||
|
userInfoUpdate: (userInfo) => ({
|
||||||
|
type: "USER_INFO_UPDATE",
|
||||||
|
userInfo,
|
||||||
|
}),
|
||||||
|
roomSave: (room) => ({
|
||||||
|
type: "ROOM_SAVE",
|
||||||
|
room,
|
||||||
|
}),
|
||||||
|
devicesUpdate: (roomId, devices, partial = false) => ({
|
||||||
|
type: "DEVICES_UPDATE",
|
||||||
|
roomId,
|
||||||
|
devices,
|
||||||
|
partial,
|
||||||
|
}),
|
||||||
|
roomDelete: (roomId) => ({
|
||||||
|
type: "ROOM_DELETE",
|
||||||
|
roomId,
|
||||||
|
}),
|
||||||
|
deviceDelete: (deviceId) => ({
|
||||||
|
type: "DEVICE_DELETE",
|
||||||
|
deviceId,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default actions;
|
|
@ -5011,7 +5011,7 @@ hmac-drbg@^1.0.0:
|
||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.0"
|
||||||
minimalistic-crypto-utils "^1.0.1"
|
minimalistic-crypto-utils "^1.0.1"
|
||||||
|
|
||||||
hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.2:
|
hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||||
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
||||||
|
@ -8747,6 +8747,11 @@ react-is@^16.8.1, react-is@^16.8.4:
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
|
||||||
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
|
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
|
||||||
|
|
||||||
|
react-is@^16.9.0:
|
||||||
|
version "16.13.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
react-popper@^1.3.4:
|
react-popper@^1.3.4:
|
||||||
version "1.3.7"
|
version "1.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324"
|
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324"
|
||||||
|
@ -8760,6 +8765,17 @@ react-popper@^1.3.4:
|
||||||
typed-styles "^0.0.7"
|
typed-styles "^0.0.7"
|
||||||
warning "^4.0.2"
|
warning "^4.0.2"
|
||||||
|
|
||||||
|
react-redux@^7.2.0:
|
||||||
|
version "7.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"
|
||||||
|
integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.5.5"
|
||||||
|
hoist-non-react-statics "^3.3.0"
|
||||||
|
loose-envify "^1.4.0"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
react-is "^16.9.0"
|
||||||
|
|
||||||
react-round-slider@^1.0.1:
|
react-round-slider@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-round-slider/-/react-round-slider-1.0.1.tgz#2f6f14f4e7ce622cc7e450911a163b5841b3fd88"
|
resolved "https://registry.yarnpkg.com/react-round-slider/-/react-round-slider-1.0.1.tgz#2f6f14f4e7ce622cc7e450911a163b5841b3fd88"
|
||||||
|
@ -8980,6 +8996,19 @@ redent@^3.0.0:
|
||||||
indent-string "^4.0.0"
|
indent-string "^4.0.0"
|
||||||
strip-indent "^3.0.0"
|
strip-indent "^3.0.0"
|
||||||
|
|
||||||
|
redux-thunk@^2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
||||||
|
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
|
||||||
|
|
||||||
|
redux@^4.0.5:
|
||||||
|
version "4.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
|
||||||
|
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.4.0"
|
||||||
|
symbol-observable "^1.2.0"
|
||||||
|
|
||||||
regenerate-unicode-properties@^8.1.0:
|
regenerate-unicode-properties@^8.1.0:
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e"
|
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e"
|
||||||
|
@ -10100,6 +10129,11 @@ svgo@^1.0.0, svgo@^1.2.2:
|
||||||
unquote "~1.1.1"
|
unquote "~1.1.1"
|
||||||
util.promisify "~1.0.0"
|
util.promisify "~1.0.0"
|
||||||
|
|
||||||
|
symbol-observable@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||||
|
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
|
||||||
|
|
||||||
symbol-tree@^3.2.2:
|
symbol-tree@^3.2.2:
|
||||||
version "3.2.4"
|
version "3.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||||
|
|
Loading…
Reference in a new issue