diff --git a/smart-hut/src/App.js b/smart-hut/src/App.js index 1365896..602e77f 100644 --- a/smart-hut/src/App.js +++ b/smart-hut/src/App.js @@ -69,7 +69,7 @@ class App extends Component { - + diff --git a/smart-hut/src/components/dashboard/Automations.css b/smart-hut/src/components/dashboard/Automations.css index 0042cba..508b97a 100644 --- a/smart-hut/src/components/dashboard/Automations.css +++ b/smart-hut/src/components/dashboard/Automations.css @@ -1,18 +1,18 @@ .segment-automations { - top: 10%; + top: 10%; } .list-index { - font-size: 1.5rem; + font-size: 1.5rem; } .remove-icon { - display: inline !important; - margin-left: 1rem !important; + display: inline !important; + margin-left: 1rem !important; } .trigger-item { - display: flex !important; - justify-content: center !important; - align-items: center !important; + display: flex !important; + justify-content: center !important; + align-items: center !important; } diff --git a/smart-hut/src/components/dashboard/DevicePanel.js b/smart-hut/src/components/dashboard/DevicePanel.js index 3108a48..449642e 100644 --- a/smart-hut/src/components/dashboard/DevicePanel.js +++ b/smart-hut/src/components/dashboard/DevicePanel.js @@ -8,58 +8,58 @@ import { connect } from "react-redux"; import { RemoteService } from "../../remote"; class DevicePanel extends Component { - constructor(props) { - super(props); + constructor(props) { + super(props); - this.getDevices(); - } + this.getDevices(); + } - getDevices() { - if (this.props.tab === "Devices") { - this.props - .fetchDevices() - .catch((err) => console.error(`error fetching devices:`, err)); - } + getDevices() { + if (this.props.tab === "Devices") { + this.props + .fetchDevices() + .catch((err) => console.error(`error fetching devices:`, err)); } + } - render() { - return ( - - {this.props.devices.map((e, i) => { - return ( - - - - ); - })} - {!this.props.isActiveRoomHome ? ( - - - - ) : null} - - ); - } + render() { + return ( + + {this.props.devices.map((e, i) => { + return ( + + + + ); + })} + {!this.props.isActiveRoomHome ? ( + + + + ) : null} + + ); + } } const mapStateToProps = (state, _) => ({ - get devices() { - if (state.active.activeRoom === -1) { - return Object.values(state.devices); - } else { - const deviceArray = [ - ...state.rooms[state.active.activeRoom].devices, - ].sort(); - return deviceArray.map((id) => state.devices[id]); - } - }, - get isActiveRoomHome() { - return state.active.activeRoom === -1; - }, - activeRoom: state.active.activeRoom, + get devices() { + if (state.active.activeRoom === -1) { + return Object.values(state.devices); + } else { + const deviceArray = [ + ...state.rooms[state.active.activeRoom].devices, + ].sort(); + return deviceArray.map((id) => state.devices[id]); + } + }, + get isActiveRoomHome() { + return state.active.activeRoom === -1; + }, + activeRoom: state.active.activeRoom, }); const DevicePanelContainer = connect( - mapStateToProps, - RemoteService + mapStateToProps, + RemoteService )(DevicePanel); export default DevicePanelContainer; diff --git a/smart-hut/src/components/dashboard/devices/NewDevice.js b/smart-hut/src/components/dashboard/devices/NewDevice.js index ff0ef72..6dcace6 100644 --- a/smart-hut/src/components/dashboard/devices/NewDevice.js +++ b/smart-hut/src/components/dashboard/devices/NewDevice.js @@ -106,7 +106,7 @@ class NewDevice extends Component { switch: "New switch", buttonDimmer: "New button dimmer", knobDimmer: "New knob dimmer", - securityCamera: "New security camera" + securityCamera: "New security camera", }; if (this.state.deviceName === "") { @@ -188,11 +188,11 @@ class NewDevice extends Component { image: { avatar: true, src: "/img/plusMinus.svg" }, }, { - key: "securityCamera", - text: "Security Camera", - value: "securityCamera", - image: { avatar: true, src: "/img/plusMinus.svg" }, - } + key: "securityCamera", + text: "Security Camera", + value: "securityCamera", + image: { avatar: true, src: "/img/plusMinus.svg" }, + }, ]; const sensorOptions = [ { diff --git a/smart-hut/src/remote.js b/smart-hut/src/remote.js index a3c6b8c..67d0633 100644 --- a/smart-hut/src/remote.js +++ b/smart-hut/src/remote.js @@ -1,8 +1,8 @@ import smartHutStore from "./store"; import actions from "./storeActions"; import axios from "axios"; -import {endpointURL, socketURL} from "./endpoint"; -import {connect, disconnect} from "@giantmachines/redux-websocket"; +import { endpointURL, socketURL } from "./endpoint"; +import { connect, disconnect } from "@giantmachines/redux-websocket"; /** * An object returned by promise rejections in remoteservice @@ -10,176 +10,173 @@ import {connect, disconnect} from "@giantmachines/redux-websocket"; * @property {String[]} messages a list of user-friendly error messages to show; */ class RemoteError extends Error { - messages; + messages; - constructor(messages) { - super(messages.join(" - ")); - this.messages = messages; - } + constructor(messages) { + super(messages.join(" - ")); + this.messages = messages; + } } const Endpoint = { - axiosInstance: axios.create({ - baseURL: endpointURL(), - validateStatus: (status) => status >= 200 && status < 300, - }), + axiosInstance: axios.create({ + baseURL: endpointURL(), + validateStatus: (status) => status >= 200 && status < 300, + }), - /** - * Returns token for current session, null if logged out - * @returns {String|null} the token - */ - get token() { - return smartHutStore.getState().login.token; - }, + /** + * Returns token for current session, null if logged out + * @returns {String|null} the token + */ + 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 - */ - send: (method, route, query = {}, body = null) => { - if (!Endpoint.token) { - throw new Error("No token while performing authenticated request"); - } + /** + * 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 + */ + send: (method, route, query = {}, body = null) => { + if (!Endpoint.token) { + throw new Error("No token while performing authenticated request"); + } - return Endpoint.axiosInstance(route, { - method: method, - params: query, - data: ["put", "post"].indexOf(method) !== -1 ? body : null, - headers: { - Authorization: `Bearer ${Endpoint.token}`, - }, - }).then((res) => { - if (!res.data && method !== "delete") { - console.error("Response body is empty"); - return null; - } else { - return res; - } - }); - }, + return Endpoint.axiosInstance(route, { + method: method, + params: query, + data: ["put", "post"].indexOf(method) !== -1 ? body : null, + headers: { + Authorization: `Bearer ${Endpoint.token}`, + }, + }).then((res) => { + if (!res.data && method !== "delete") { + console.error("Response body is empty"); + return null; + } else { + return res; + } + }); + }, - /** - * Performs a non-authenticated post and put request for registration, reset-password - * @param {post} 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 - */ - sendNA: (method, route, query = {}, body = null) => { - return Endpoint.axiosInstance(route, { - method: method, - params: query, - data: ["put", "post"].indexOf(method) !== -1 ? body : null, - }).then((res) => { - if (!res.data) { - console.error("Response body is empty"); - return null; - } else { - return res; - } - }); - }, + /** + * Performs a non-authenticated post and put request for registration, reset-password + * @param {post} 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 + */ + sendNA: (method, route, query = {}, body = null) => { + return Endpoint.axiosInstance(route, { + method: method, + params: query, + data: ["put", "post"].indexOf(method) !== -1 ? body : null, + }).then((res) => { + if (!res.data) { + console.error("Response body is empty"); + return null; + } else { + return res; + } + }); + }, - /** - * Performs login - * @param {String} usernameOrEmail - * @param {String} password - * @returns {Promise} promise that resolves to the token string - * and rejects to the axios error. - */ - login: (usernameOrEmail, password) => { - return Endpoint.axiosInstance - .post(`/auth/login`, { - usernameOrEmail, - password, - }) - .then((res) => { - localStorage.setItem("token", res.data.jwttoken); - localStorage.setItem( - "exp", - new Date().getTime() + 5 * 60 * 60 * 1000 - ); - return res.data.jwttoken; - }); - }, + /** + * Performs login + * @param {String} usernameOrEmail + * @param {String} password + * @returns {Promise} promise that resolves to the token string + * and rejects to the axios error. + */ + login: (usernameOrEmail, password) => { + return Endpoint.axiosInstance + .post(`/auth/login`, { + usernameOrEmail, + password, + }) + .then((res) => { + localStorage.setItem("token", res.data.jwttoken); + localStorage.setItem("exp", new Date().getTime() + 5 * 60 * 60 * 1000); + return res.data.jwttoken; + }); + }, - /** - * Returns an immediately resolved promise for the socket logouts - * @return {Promise} An always-resolved promise - */ - logout: () => { - localStorage.removeItem("token"); - localStorage.removeItem("exp"); - return Promise.resolve(void 0); - }, + /** + * Returns an immediately resolved promise for the socket logouts + * @return {Promise} An always-resolved promise + */ + logout: () => { + localStorage.removeItem("token"); + localStorage.removeItem("exp"); + return Promise.resolve(void 0); + }, - /** - * 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) - * @returns {Promise<*, *>} The Axios-generated promise - */ - get(route, query = {}) { - return this.send("get", route, query); - }, + /** + * 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) + * @returns {Promise<*, *>} The Axios-generated promise + */ + 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 - * @returns {Promise<*, *>} The Axios-generated promise - */ - post(route, query, body) { - return this.send("post", route, query, body); - }, + /** + * 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 + * @returns {Promise<*, *>} The Axios-generated promise + */ + post(route, query, body) { + return this.send("post", route, query, body); + }, - /** - * Performs a non-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 - * @returns {Promise<*, *>} The Axios-generated promise - */ - postNA(route, query, body) { - return this.sendNA("post", route, query, body); - }, + /** + * Performs a non-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 + * @returns {Promise<*, *>} The Axios-generated promise + */ + postNA(route, query, body) { + return this.sendNA("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 - * @returns {Promise<*, *>} The Axios-generated promise - */ - put(route, query = {}, body = {}) { - return this.send("put", 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 + * @returns {Promise<*, *>} The Axios-generated promise + */ + put(route, query = {}, body = {}) { + return this.send("put", route, query, body); + }, - /** - * Performs a non-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 - * @returns {Promise<*, *>} The Axios-generated promise - */ - putNA(route, query = {}, body = {}) { - return this.sendNA("put", route, query, body); - }, + /** + * Performs a non-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 + * @returns {Promise<*, *>} The Axios-generated promise + */ + putNA(route, query = {}, body = {}) { + return this.sendNA("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) - * @returns {Promise<*, *>} The Axios-generated promise - */ - delete(route, query = {}) { - return this.send("delete", route, query); - }, + /** + * 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) + * @returns {Promise<*, *>} The Axios-generated promise + */ + delete(route, query = {}) { + return this.send("delete", route, query); + }, }; /** @@ -189,663 +186,644 @@ const Endpoint = { * @returns {RemoteError} user friendly error messages */ function parseValidationErrors(err) { - if ( - err.response && - err.response.status === 400 && - err.response.data && - Array.isArray(err.response.data.errors) - ) { - throw new RemoteError([ - ...new Set(err.response.data.errors.map((e) => e.defaultMessage)), - ]); - } else { - console.warn("Non validation error", err); - throw new RemoteError(["Network error"]); - } + if ( + err.response && + err.response.status === 400 && + err.response.data && + Array.isArray(err.response.data.errors) + ) { + throw new RemoteError([ + ...new Set(err.response.data.errors.map((e) => e.defaultMessage)), + ]); + } else { + console.warn("Non validation error", err); + throw new RemoteError(["Network error"]); + } } export const RemoteService = { - /** - * Performs login - * @param {String} usernameOrEmail - * @param {String} password - * @returns {Promise} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - login: (usernameOrEmail, password) => { - return (dispatch) => { - return Endpoint.login(usernameOrEmail, password) - .then((token) => { - dispatch(actions.loginSuccess(token)); - dispatch(connect(socketURL(token))); - }) - .catch((err) => { - console.warn("login error", err); - throw new RemoteError([ - err.response && err.response.status === 401 - ? "Wrong credentials" - : "An error occurred while logging in", - ]); - }); - }; - }, + /** + * Performs login + * @param {String} usernameOrEmail + * @param {String} password + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + login: (usernameOrEmail, password) => { + return (dispatch) => { + return Endpoint.login(usernameOrEmail, password) + .then((token) => { + dispatch(actions.loginSuccess(token)); + dispatch(connect(socketURL(token))); + }) + .catch((err) => { + console.warn("login error", err); + throw new RemoteError([ + err.response && err.response.status === 401 + ? "Wrong credentials" + : "An error occurred while logging in", + ]); + }); + }; + }, - /** - * Performs logout - */ - logout: () => { - return (dispatch) => - Endpoint.logout().then(() => { - dispatch(disconnect()); - dispatch(actions.logout()); + /** + * Performs logout + */ + logout: () => { + return (dispatch) => + Endpoint.logout().then(() => { + dispatch(disconnect()); + dispatch(actions.logout()); + }); + }, + + /** + * Fetches user information via REST calls, if it is logged in + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + 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); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * Fetches all rooms that belong to this user. This call does not + * populate the devices attribute in rooms. + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + fetchAllRooms: () => { + return (dispatch) => { + return Endpoint.get("/room") + .then((res) => void dispatch(actions.roomsUpdate(res.data))) + .catch((err) => { + console.error("Fetch all rooms error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * Fetches all scenes that belong to this user. This call does not + * populate the devices attribute in scenes. + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + fetchAllScenes: () => { + return (dispatch) => { + return Endpoint.get("/scene") + .then((res) => void dispatch(actions.scenesUpdate(res.data))) + .catch((err) => { + console.error("Fetch all scenes error", err); + throw new RemoteError(["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 rsoom to which fetch devices + * from, null to fetch from all rooms + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + 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); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * Fetches all the automations + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + fetchAutomations: () => { + return (dispatch) => { + return Endpoint.get("/automation/") + .then((res) => { + const length = res.data.length; + const automations = []; + + res.data.forEach((a, index) => { + const { id, name } = a; + const automation = { + name, + id, + triggers: [], + scenes: [], + }; + + return Endpoint.get(`/booleanTrigger/${id}`).then((res) => { + automation.triggers.push(...res.data); + return Endpoint.get(`/rangeTrigger/${id}`).then((res) => { + automation.triggers.push(...res.data); + return Endpoint.get(`/scenePriority/${id}`).then((res) => { + automation.scenes.push(...res.data); + automations.push(automation); + if (index + 1 === length) { + return void dispatch( + actions.automationsUpdate(automations) + ); + } }); - }, + }); + }); + }); + }) + .catch((err) => { + console.error(`Fetch automations error`, err); + throw new RemoteError(["Network error"]); + }); + }; + }, - /** - * Fetches user information via REST calls, if it is logged in - * @returns {Promise} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - 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); - throw new RemoteError(["Network error"]); - }); + /** + * Fetches all devices in a particular scene, or fetches all devices. + * This also updates the devices attribute on values in the map scenes. + * @param {Number} sceneId the scene to which fetch devices + * from, null to fetch from all scenes + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + fetchStates: (sceneId) => { + return (dispatch) => { + return Endpoint.get(`/scene/${sceneId}/states`) + .then((res) => void dispatch(actions.statesUpdate(sceneId, res.data))) + .catch((err) => { + console.error(`Fetch devices sceneId=${sceneId} error`, err); + throw new RemoteError(["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} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + 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 scene with the given data + * @param {String} data.name the scene's name, + * @param {Number|null} sceneId the scene's id if update, null for creation + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + saveScene: (data, sceneId = null) => { + return (dispatch) => { + data = { + name: data.name, + }; + + return (sceneId + ? Endpoint.put(`/scene/${sceneId}`, {}, data) + : Endpoint.post(`/scene`, {}, data) + ) + .then((res) => void dispatch(actions.sceneSave(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 "/" + * endpoints are used for creation. + * @param {Device} data the device to update. + * @returns {Promise} promise that resolves to the saved device and rejects + * with user-fiendly errors as a RemoteError + */ + saveDevice: (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) => { + dispatch(actions.deviceSave(res.data)); + return res.data; + }) + .catch((err) => { + console.warn("Update device: ", data, "error: ", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * Creates/Updates an automation with the given data. If + * data.id is truthy, then a update call is performed, + * otherwise a create call is performed. + * @param {Automation} data the device to update. + * @returns {Promise} promise that resolves to the saved device and rejects + * with user-fiendly errors as a RemoteError + */ + saveAutomation: (data) => { + const { automation, triggerList, order } = data; + console.log("automation: ", automation, triggerList, order); + automation.triggers = []; + automation.scenes = []; + return (dispatch) => { + let urlAutomation = "/automation"; + let urlBooleanTrigger = "/booleanTrigger"; + let urlRangeTrigger = "/rangeTrigger"; + let urlScenePriority = "/scenePriority"; + + let rangeTriggerList = triggerList.filter((trigger) => + trigger.hasOwnProperty("operand") + ); + let booleanTriggerList = triggerList.filter( + (trigger) => !trigger.hasOwnProperty("operand") + ); + + return Endpoint["post"](urlAutomation, {}, automation).then( + async (automationRes) => { + const { id } = automationRes.data; + // Introduce the range triggers in the automation + for (let t of rangeTriggerList) { + const trigger = { + automationId: id, + deviceId: t.device, + operator: t.operand, + range: t.value, }; - }, + let resRange = await Endpoint.post(urlRangeTrigger, {}, trigger); + automation.triggers.push(resRange.data); + } - /** - * Fetches all rooms that belong to this user. This call does not - * populate the devices attribute in rooms. - * @returns {Promise} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - fetchAllRooms: () => { - return (dispatch) => { - return Endpoint.get("/room") - .then((res) => void dispatch(actions.roomsUpdate(res.data))) - .catch((err) => { - console.error("Fetch all rooms error", err); - throw new RemoteError(["Network error"]); - }); + for (let t of booleanTriggerList) { + const trigger = { + automationId: id, + deviceId: t.device, + on: t.value, }; - }, - - /** - * Fetches all scenes that belong to this user. This call does not - * populate the devices attribute in scenes. - * @returns {Promise} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - fetchAllScenes: () => { - return (dispatch) => { - return Endpoint.get("/scene") - .then((res) => void dispatch(actions.scenesUpdate(res.data))) - .catch((err) => { - console.error("Fetch all scenes error", err); - throw new RemoteError(["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 rsoom to which fetch devices - * from, null to fetch from all rooms - * @returns {Promise} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - 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); - throw new RemoteError(["Network error"]); - }); - }; - }, - - /** - * Fetches all the automations - * @returns {Promise} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - fetchAutomations: () => { - return (dispatch) => { - return Endpoint.get("/automation/") - .then( - (res) => { - const length = res.data.length; - const automations = []; - - res.data.forEach((a, index) => { - const {id, name} = a; - const automation = { - name, - id, - triggers: [], - scenes: [] - } - - return Endpoint.get(`/booleanTrigger/${id}`) - .then(res => { - automation.triggers.push(...res.data); - return Endpoint.get(`/rangeTrigger/${id}`) - .then(res => { - automation.triggers.push(...res.data); - return Endpoint.get(`/scenePriority/${id}`) - .then(res => { - automation.scenes.push(...res.data); - automations.push(automation); - if ((index + 1) === length) { - return void dispatch(actions.automationsUpdate(automations)); - } - }) - - }) - - }); - }); - } - ) - .catch((err) => { - console.error(`Fetch automations error`, err); - throw new RemoteError(["Network error"]); - }); - }; - }, - - /** - * Fetches all devices in a particular scene, or fetches all devices. - * This also updates the devices attribute on values in the map scenes. - * @param {Number} sceneId the scene to which fetch devices - * from, null to fetch from all scenes - * @returns {Promise} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - fetchStates: (sceneId) => { - return (dispatch) => { - return Endpoint.get(`/scene/${sceneId}/states`) - .then( - (res) => - void dispatch(actions.statesUpdate(sceneId, res.data)) - ) - .catch((err) => { - console.error( - `Fetch devices sceneId=${sceneId} error`, - err - ); - throw new RemoteError(["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} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - 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 scene with the given data - * @param {String} data.name the scene's name, - * @param {Number|null} sceneId the scene's id if update, null for creation - * @returns {Promise} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - saveScene: (data, sceneId = null) => { - return (dispatch) => { - data = { - name: data.name, - }; - - return (sceneId - ? Endpoint.put(`/scene/${sceneId}`, {}, data) - : Endpoint.post(`/scene`, {}, data) - ) - .then((res) => void dispatch(actions.sceneSave(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 "/" - * endpoints are used for creation. - * @param {Device} data the device to update. - * @returns {Promise} promise that resolves to the saved device and rejects - * with user-fiendly errors as a RemoteError - */ - saveDevice: (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) => { - dispatch(actions.deviceSave(res.data)); - return res.data; - }) - .catch((err) => { - console.warn("Update device: ", data, "error: ", err); - throw new RemoteError(["Network error"]); - }); - }; - }, - - - /** - * Creates/Updates an automation with the given data. If - * data.id is truthy, then a update call is performed, - * otherwise a create call is performed. - * @param {Automation} data the device to update. - * @returns {Promise} promise that resolves to the saved device and rejects - * with user-fiendly errors as a RemoteError - */ - saveAutomation: (data) => { - const {automation, triggerList, order} = data; - console.log("automation: ", automation, triggerList, order); - automation.triggers = []; - automation.scenes = []; - return (dispatch) => { - let urlAutomation = "/automation"; - let urlBooleanTrigger = "/booleanTrigger"; - let urlRangeTrigger = "/rangeTrigger"; - let urlScenePriority = "/scenePriority"; - - let rangeTriggerList = triggerList.filter(trigger => trigger.hasOwnProperty("operand")); - let booleanTriggerList = triggerList.filter(trigger => !trigger.hasOwnProperty("operand")); - - return Endpoint["post"](urlAutomation, {}, automation) - .then(async automationRes => { - const {id} = automationRes.data; - // Introduce the range triggers in the automation - for (let t of rangeTriggerList) { - const trigger = { - automationId: id, - deviceId: t.device, - operator: t.operand, - range: t.value - }; - let resRange = await Endpoint.post(urlRangeTrigger, {}, trigger) - automation.triggers.push(resRange.data); - } - - for (let t of booleanTriggerList) { - const trigger = { - automationId: id, - deviceId: t.device, - on: t.value - }; - let resBoolean = await Endpoint.post(urlBooleanTrigger, {}, trigger) - automation.triggers.push(resBoolean.data); - console.log("TRIGGERS: ", automation); - } - - for (let [priority, sceneId] of order.entries()) { - const scenePriority = { - automationId: id, - priority, - sceneId, - } - let resScenes = await Endpoint["post"](urlScenePriority, {}, scenePriority) - automation.scenes.push(resScenes.data); - } - automation.id = id; - dispatch(actions.automationSave(automation)); - - }); - - } - }, - - /** - * Creates/Updates a state 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 "/" - * endpoints are used for creation. - * @param {State} data the device to update. - * @returns {Promise} promise that resolves to the saved device and rejects - * with user-fiendly errors as a RemoteError - */ - saveState: (data) => { - return (dispatch) => { - let url = - "/" + - data.kind + - "/" + - data.id + - "/state?sceneId=" + - data.sceneId; - - return Endpoint["post"](url, {}, data) - .then((res) => { - dispatch(actions.stateSave(res.data)); - return res.data; - }) - .catch((err) => { - console.warn("Update device: ", data, "error: ", err); - throw new RemoteError(["Network error"]); - }); - }; - }, - - /** - * Connetcs a series of output devices to an input device. - * Output devices for Switch input can be: Normal Light, Dimmable Light, Smart Plug. - * Output devices for Dimmers input can be: Dimmable Light. - * - * @typedef {"switch" | "buttonDimmer" | "knobDimmer"} ConnectableInput - * - * @param {ConnectableInput} newDevice.kind kind of the input device - * @param {Integer} newDevice.id id of the input device - * @param {Integer[]} outputs ids of the output device - * @returns {Promise} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - connectOutputs: (newDevice, outputs) => { - return (dispatch) => { - let url = `/${newDevice.kind}/${newDevice.id}/lights`; - - return Endpoint.post(url, {}, outputs) - .then((res) => { - dispatch(actions.deviceOperationUpdate(res.data)); - return res.data; - }) - .catch((err) => { - console.warn( - "ConnectOutputs of ", - newDevice.id, - " with outputs: ", - outputs, - "error: ", - err - ); - throw new RemoteError(["Network error"]); - }); - }; - }, - - _operateInput: (url, getUrl, action) => { - return (dispatch) => { - return Endpoint.put(url, {}, action) - .then(async (res) => { - const inputDevice = await Endpoint.get(getUrl); - delete inputDevice.outputs; - dispatch( - actions.deviceOperationUpdate([ - ...res.data, - inputDevice.data, - ]) - ); - }) - .catch((err) => { - console.warn(`${url} error`, err); - throw new RemoteError(["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} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - switchOperate: (switchId, type) => { - return RemoteService._operateInput( - "/switch/operate", - `/switch/${switchId}`, - { - type: type.toUpperCase(), - id: switchId, - } + let resBoolean = await Endpoint.post( + urlBooleanTrigger, + {}, + trigger ); - }, + automation.triggers.push(resBoolean.data); + console.log("TRIGGERS: ", automation); + } - /** - * 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} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - knobDimmerDimTo: (dimmerId, intensity) => { - return RemoteService._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} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - buttonDimmerDim: (dimmerId, dimType) => { - return RemoteService._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} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - 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); - throw new RemoteError(["Network error"]); - }); + for (let [priority, sceneId] of order.entries()) { + const scenePriority = { + automationId: id, + priority, + sceneId, }; + let resScenes = await Endpoint["post"]( + urlScenePriority, + {}, + scenePriority + ); + automation.scenes.push(resScenes.data); + } + automation.id = id; + dispatch(actions.automationSave(automation)); } - , + ); + }; + }, - /** - * Deletes a room - * @param {Number} roomId the id of the room to delete - * @returns {Promise} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - deleteRoom: (roomId) => { - return (dispatch) => { - return Endpoint.delete(`/room/${roomId}`) - .then((_) => dispatch(actions.roomDelete(roomId))) - .catch((err) => { - console.warn("Room deletion error", err); - throw new RemoteError(["Network error"]); - }); - }; - }, + /** + * Creates/Updates a state 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 "/" + * endpoints are used for creation. + * @param {State} data the device to update. + * @returns {Promise} promise that resolves to the saved device and rejects + * with user-fiendly errors as a RemoteError + */ + saveState: (data) => { + return (dispatch) => { + let url = + "/" + data.kind + "/" + data.id + "/state?sceneId=" + data.sceneId; - deleteAutomation: (id) => { - console.log("ID OF AUTO ", id); - return (dispatch) => { - return Endpoint.delete(`/automation/${id}`) - .then((_) => dispatch(actions.automationDelete(id))) - .catch((err) => { - console.warn("Automation deletion error", err); - throw new RemoteError(["Network error"]); - }); - }; + return Endpoint["post"](url, {}, data) + .then((res) => { + dispatch(actions.stateSave(res.data)); + return res.data; + }) + .catch((err) => { + console.warn("Update device: ", data, "error: ", err); + throw new RemoteError(["Network error"]); + }); + }; + }, - }, + /** + * Connetcs a series of output devices to an input device. + * Output devices for Switch input can be: Normal Light, Dimmable Light, Smart Plug. + * Output devices for Dimmers input can be: Dimmable Light. + * + * @typedef {"switch" | "buttonDimmer" | "knobDimmer"} ConnectableInput + * + * @param {ConnectableInput} newDevice.kind kind of the input device + * @param {Integer} newDevice.id id of the input device + * @param {Integer[]} outputs ids of the output device + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + connectOutputs: (newDevice, outputs) => { + return (dispatch) => { + let url = `/${newDevice.kind}/${newDevice.id}/lights`; - /** - * Deletes a scene - * @param {Number} sceneId the id of the scene to delete - * @returns {Promise} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - deleteScene: (sceneId) => { - return (dispatch) => { - return Endpoint.delete(`/scene/${sceneId}`) - .then((_) => dispatch(actions.sceneDelete(sceneId))) - .catch((err) => { - console.warn("Scene deletion error", err); - throw new RemoteError(["Network error"]); - }); - }; - }, + return Endpoint.post(url, {}, outputs) + .then((res) => { + dispatch(actions.deviceOperationUpdate(res.data)); + return res.data; + }) + .catch((err) => { + console.warn( + "ConnectOutputs of ", + newDevice.id, + " with outputs: ", + outputs, + "error: ", + err + ); + throw new RemoteError(["Network error"]); + }); + }; + }, - /** - * Deletes a device - * @param {Device} device the device to delete - * @returns {Promise} promise that resolves to void and rejects - * with user-fiendly errors as a RemoteError - */ - deleteDevice: (device) => { - return (dispatch) => { - return Endpoint.delete(`/${device.kind}/${device.id}`) - .then((_) => dispatch(actions.deviceDelete(device.id))) - .catch((err) => { - console.warn("Device deletion error", err); - throw new RemoteError(["Network error"]); - }); - }; - }, - } -; + _operateInput: (url, getUrl, action) => { + return (dispatch) => { + return Endpoint.put(url, {}, action) + .then(async (res) => { + const inputDevice = await Endpoint.get(getUrl); + delete inputDevice.outputs; + dispatch( + actions.deviceOperationUpdate([...res.data, inputDevice.data]) + ); + }) + .catch((err) => { + console.warn(`${url} error`, err); + throw new RemoteError(["Network error"]); + }); + }; + }, -for (const key in RemoteService - ) { - RemoteService[key] = RemoteService[key].bind(RemoteService); + /** + * 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} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + switchOperate: (switchId, type) => { + return RemoteService._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} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + knobDimmerDimTo: (dimmerId, intensity) => { + return RemoteService._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} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + buttonDimmerDim: (dimmerId, dimType) => { + return RemoteService._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} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + 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); + throw new RemoteError(["Network error"]); + }); + }; + }, + /** + * Deletes a room + * @param {Number} roomId the id of the room to delete + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + deleteRoom: (roomId) => { + return (dispatch) => { + return Endpoint.delete(`/room/${roomId}`) + .then((_) => dispatch(actions.roomDelete(roomId))) + .catch((err) => { + console.warn("Room deletion error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + deleteAutomation: (id) => { + console.log("ID OF AUTO ", id); + return (dispatch) => { + return Endpoint.delete(`/automation/${id}`) + .then((_) => dispatch(actions.automationDelete(id))) + .catch((err) => { + console.warn("Automation deletion error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * Deletes a scene + * @param {Number} sceneId the id of the scene to delete + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + deleteScene: (sceneId) => { + return (dispatch) => { + return Endpoint.delete(`/scene/${sceneId}`) + .then((_) => dispatch(actions.sceneDelete(sceneId))) + .catch((err) => { + console.warn("Scene deletion error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * Deletes a device + * @param {Device} device the device to delete + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + deleteDevice: (device) => { + return (dispatch) => { + return Endpoint.delete(`/${device.kind}/${device.id}`) + .then((_) => dispatch(actions.deviceDelete(device.id))) + .catch((err) => { + console.warn("Device deletion error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, +}; + +for (const key in RemoteService) { + RemoteService[key] = RemoteService[key].bind(RemoteService); } 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} promise that resolves to void and rejects - * with validation errors as a String array - */ - static submitRegistration(data) { - return Endpoint.postNA( - "/register", - {}, - { - username: data.username, - password: data.password, - name: data.name, - email: data.email, - } - ) - .then((_) => void 0) - .catch(parseValidationErrors); - } + /** + * 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} promise that resolves to void and rejects + * with validation errors as a String array + */ + static submitRegistration(data) { + return Endpoint.postNA( + "/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} promise that resolves to void and rejects - * with validation errors as a String array - */ - static submitInitResetPassword(email) { - return Endpoint.postNA( - "/register/init-reset-password", - {}, - { - email: email, - } - ) - .then((_) => void 0) - .catch((err) => { - console.warn("Init reset password failed", err); - throw new RemoteError(["Network error"]); - }); - } + /** + * 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} promise that resolves to void and rejects + * with validation errors as a String array + */ + static submitInitResetPassword(email) { + return Endpoint.postNA( + "/register/init-reset-password", + {}, + { + email: email, + } + ) + .then((_) => void 0) + .catch((err) => { + console.warn("Init reset password failed", err); + throw new RemoteError(["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} confirmationToken the confirmation token got from the email - * @param {String} password the new password - * @returns {Promise} promise that resolves to void and rejects - * with validation errors as a String array - */ - static submitResetPassword(confirmationToken, password) { - return Endpoint.putNA( - "/register/reset-password", - {}, - { - confirmationToken, - password, - } - ) - .then((_) => void 0) - .catch(parseValidationErrors); - } + /** + * 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} confirmationToken the confirmation token got from the email + * @param {String} password the new password + * @returns {Promise} promise that resolves to void and rejects + * with validation errors as a String array + */ + static submitResetPassword(confirmationToken, password) { + return Endpoint.putNA( + "/register/reset-password", + {}, + { + confirmationToken, + password, + } + ) + .then((_) => void 0) + .catch(parseValidationErrors); + } } diff --git a/smart-hut/src/views/Dashboard.js b/smart-hut/src/views/Dashboard.js index af0a3fa..1d97806 100644 --- a/smart-hut/src/views/Dashboard.js +++ b/smart-hut/src/views/Dashboard.js @@ -8,8 +8,8 @@ import AutomationsNavbar from "./AutomationsNavbar"; import MyHeader from "../components/HeaderController"; import { Grid, Responsive, Button } from "semantic-ui-react"; import { - panelStyle, - mobilePanelStyle, + panelStyle, + mobilePanelStyle, } from "../components/dashboard/devices/styleComponents"; import { RemoteService } from "../remote"; @@ -17,203 +17,177 @@ import { connect } from "react-redux"; import { appActions } from "../storeActions"; class Dashboard extends Component { - constructor(props) { - super(props); - this.state = this.initialState; - this.setInitialState(); - this.activeTab = "Automations"; //TODO Remove this to not put automations first - this.selectTab = this.selectTab.bind(this); - } + constructor(props) { + super(props); + this.state = this.initialState; + this.setInitialState(); + this.activeTab = "Automations"; //TODO Remove this to not put automations first + this.selectTab = this.selectTab.bind(this); + } - get initialState() { - return { - activeTab: this.activeTab, - }; - } + get initialState() { + return { + activeTab: this.activeTab, + }; + } - setInitialState() { - this.setState(this.initialState); - } + setInitialState() { + this.setState(this.initialState); + } - get activeTab() { - return this.props.activeTab; - } + get activeTab() { + return this.props.activeTab; + } - set activeTab(tab) { - this.props.setActiveTab(tab); - } + set activeTab(tab) { + this.props.setActiveTab(tab); + } - selectTab(e, { name }) { - this.setState({ activeTab: name }); - this.activeTab = name; - } + selectTab(e, { name }) { + this.setState({ activeTab: name }); + this.activeTab = name; + } - renderTab(tab) { - switch (tab) { - case "Devices": - return ; - case "Scenes": - return ; - case "Automations": - return ; - default: - return

ERROR

; - } + renderTab(tab) { + switch (tab) { + case "Devices": + return ; + case "Scenes": + return ; + case "Automations": + return ; + default: + return

ERROR

; } + } - renderNavbar(tab) { - switch (tab) { - case "Devices": - return ; - case "Scenes": - return ; - case "Automations": - return ; - default: - return

ERROR

; - } + renderNavbar(tab) { + switch (tab) { + case "Devices": + return ; + case "Scenes": + return ; + case "Automations": + return ; + default: + return

ERROR

; } + } - render() { - return ( -
- - - - - - - - - - -
- ); - } + +
{this.renderTab(this.activeTab)}
+
+ + + + + + + + + + + + +