From 2e63d602732b2c87acb06704ee58069a6cae3468 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 4 Jun 2023 15:22:09 +0200 Subject: [PATCH] alova implementation, testing --- interface/.env.development | 2 + interface/package.json | 4 +- interface/public/css/roboto.css | 2 +- interface/src/api/endpoints.ts | 60 +++++++- interface/src/api/unpack.ts | 3 - interface/src/project/DashboardDevices.tsx | 135 ++++++++++++------ .../src/project/DashboardDevicesDialog.tsx | 32 ++++- interface/src/project/api.ts | 45 +++--- interface/src/project/types.ts | 2 + interface/yarn.lock | 116 ++++++++------- mock-api/server.js | 23 ++- 11 files changed, 280 insertions(+), 144 deletions(-) create mode 100644 interface/.env.development diff --git a/interface/.env.development b/interface/.env.development new file mode 100644 index 000000000..19cc804fe --- /dev/null +++ b/interface/.env.development @@ -0,0 +1,2 @@ +VITE_ALOVA_TIPS=0 +REACT_APP_ALOVA_TIPS=0 \ No newline at end of file diff --git a/interface/package.json b/interface/package.json index 0898b2c16..ab3303400 100644 --- a/interface/package.json +++ b/interface/package.json @@ -19,6 +19,7 @@ "lint": "eslint . --cache --fix" }, "dependencies": { + "@alova/adapter-xhr": "^1.0.0", "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.11.16", @@ -29,6 +30,7 @@ "@types/react": "^18.2.8", "@types/react-dom": "^18.2.4", "@types/react-router-dom": "^5.3.3", + "alova": "^2.5.4", "async-validator": "^4.2.5", "axios": "^1.4.0", "history": "^5.3.0", @@ -47,7 +49,7 @@ "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.59.8", "@typescript-eslint/parser": "^5.59.8", - "@vitejs/plugin-react-swc": "^3.3.1", + "@vitejs/plugin-react-swc": "^3.3.2", "eslint": "^8.42.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^17.0.0", diff --git a/interface/public/css/roboto.css b/interface/public/css/roboto.css index dd5649d0c..0c05736a7 100644 --- a/interface/public/css/roboto.css +++ b/interface/public/css/roboto.css @@ -8,7 +8,7 @@ font-style: normal; font-weight: 400; /* src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxK.woff2) format('woff2'); */ - src: local('Roboto'), local('Roboto-Regular'), url(../fonts/re.off2) format('woff2'); + src: local('Roboto'), local('Roboto-Regular'), url(../fonts/re.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+011E-011F, U+0130-0131, U+0141-0144, U+0152-0153, U+015A-015B, U+015E-015F, U+0179-017C, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; diff --git a/interface/src/api/endpoints.ts b/interface/src/api/endpoints.ts index 7c003693b..19ce5b290 100644 --- a/interface/src/api/endpoints.ts +++ b/interface/src/api/endpoints.ts @@ -1,12 +1,18 @@ +import { xhrRequestAdapter } from '@alova/adapter-xhr'; +import { createAlova, useRequest } from 'alova'; +import GlobalFetch from 'alova/GlobalFetch'; +import ReactHook from 'alova/react'; import axios from 'axios'; import { unpack } from './unpack'; import type { AxiosPromise, CancelToken, AxiosProgressEvent } from 'axios'; export const WS_BASE_URL = '/ws/'; -export const API_BASE_URL = '/rest/'; export const ES_BASE_URL = '/es/'; -export const EMSESP_API_BASE_URL = '/api/'; + +export const REST_BASE_URL = '/rest/'; +export const API_BASE_URL = '/api/'; + export const ACCESS_TOKEN = 'access_token'; const location = window.location; @@ -14,8 +20,47 @@ const webProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; export const WEB_SOCKET_ROOT = webProtocol + '//' + location.host + WS_BASE_URL; export const EVENT_SOURCE_ROOT = location.protocol + '//' + location.host + ES_BASE_URL; +export const alovaInstance = createAlova({ + baseURL: '/rest/', + statesHook: ReactHook, + requestAdapter: xhrRequestAdapter(), + // requestAdapter: GlobalFetch(), + beforeRequest(method) { + // TODO check if bearer works + if (localStorage.getItem(ACCESS_TOKEN)) { + method.config.headers.token = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN); + } + }, + responsed: (response) => response.data + + // TODO add error handling for Push? + + // return JSON.stringify(response.data); + + // responded: { + // // When using the GlobalFetch request adapter, the first parameter receives the Response object + // // The second parameter is the method instance of the current request, you can use it to synchronize the configuration information before and after the request + // onSuccess: async (response, method) => { + // if (response.status >= 400) { + // throw new Error(response.statusText); + // } + // console.log('response', response); + // const json = await response.json(); + // // The parsed response data will be passed to the transformData hook function of the method instance, and these functions will be explained later + // return json; + // }, + + // // Interceptor for request failure + // // This interceptor will be entered when the request is wrong. + // // The second parameter is the method instance of the current request, you can use it to synchronize the configuration information before and after the request + // onError: (error, method) => { + // alert(error.message); + // } + // } +}); + export const AXIOS = axios.create({ - baseURL: API_BASE_URL, + baseURL: REST_BASE_URL, headers: { 'Content-Type': 'application/json' }, @@ -35,7 +80,7 @@ export const AXIOS = axios.create({ }); export const AXIOS_API = axios.create({ - baseURL: EMSESP_API_BASE_URL, + baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json' }, @@ -55,7 +100,7 @@ export const AXIOS_API = axios.create({ }); export const AXIOS_BIN = axios.create({ - baseURL: API_BASE_URL, + baseURL: REST_BASE_URL, headers: { 'Content-Type': 'application/json' }, @@ -73,10 +118,11 @@ export const AXIOS_BIN = axios.create({ return JSON.stringify(data); } ], - // transformResponse: [(data) => decode(data)] - transformResponse: [(data) => unpack(data)] // new using msgpackr + transformResponse: [(data) => unpack(data)] }); +// TODO replace with alova +// TODO see https://alova.js.org/next-step/download-upload-progress export interface FileUploadConfig { cancelToken?: CancelToken; onUploadProgress?: (progressEvent: AxiosProgressEvent) => void; diff --git a/interface/src/api/unpack.ts b/interface/src/api/unpack.ts index d28619786..a25078d5d 100644 --- a/interface/src/api/unpack.ts +++ b/interface/src/api/unpack.ts @@ -968,7 +968,6 @@ currentExtensions[0x69] = (data) => { if (!referenceMap) referenceMap = new Map(); const token = src[position]; let target; - // TODO: handle Maps, Sets, and other types that can cycle; this is complicated, because you potentially need to read // ahead past references to record structure definitions if ((token >= 0x90 && token < 0xa0) || token == 0xdc || token == 0xdd) target = []; else target = {}; @@ -1041,7 +1040,6 @@ currentExtensions[0xff] = (data) => { ((data[3] & 0x3) * 0x100000000 + data[4] * 0x1000000 + (data[5] << 16) + (data[6] << 8) + data[7]) * 1000 ); else if (data.length == 12) - // TODO: Implement support for negative return new Date( ((data[0] << 24) + (data[1] << 16) + (data[2] << 8) + data[3]) / 1000000 + ((data[4] & 0x80 ? -0x1000000000000 : 0) + @@ -1070,7 +1068,6 @@ function saveState(callback) { const savedReferenceMap = referenceMap; const savedBundledStrings = bundledStrings; - // TODO: We may need to revisit this if we do more external calls to user code (since it could be slow) const savedSrc = new Uint8Array(src.slice(0, srcEnd)); // we copy the data in case it changes while external data is processed const savedStructures = currentStructures; const savedStructuresContents = currentStructures.slice(0, currentStructures.length); diff --git a/interface/src/project/DashboardDevices.tsx b/interface/src/project/DashboardDevices.tsx index b49af0d35..1b7dd2013 100644 --- a/interface/src/project/DashboardDevices.tsx +++ b/interface/src/project/DashboardDevices.tsx @@ -30,6 +30,7 @@ import { useRowSelect } from '@table-library/react-table-library/select'; import { useSort, SortToggleType } from '@table-library/react-table-library/sort'; import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; import { useTheme } from '@table-library/react-table-library/theme'; +import { useRequest } from 'alova'; import { useState, useContext, useEffect, useCallback, useLayoutEffect } from 'react'; import { IconContext } from 'react-icons'; @@ -54,17 +55,45 @@ const DashboardDevices: FC = () => { const [size, setSize] = useState([0, 0]); const { me } = useContext(AuthenticatedContext); const { LL } = useI18nContext(); - const [deviceData, setDeviceData] = useState({ data: [] }); const [selectedDeviceValue, setSelectedDeviceValue] = useState(); const [onlyFav, setOnlyFav] = useState(false); const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState(false); const [showDeviceInfo, setShowDeviceInfo] = useState(false); const [selectedDevice, setSelectedDevice] = useState(); - const [coreData, setCoreData] = useState({ - connected: true, - devices: [] + + // TODO remove + // const [deviceData, setDeviceData] = useState({ data: [] }); + // const [coreData, setCoreData] = useState({ + // connected: true, + // devices: [] + // }); + + const { data: coreData, send: readCoreData } = useRequest(() => EMSESP.readCoreData(), { + initialData: { + connected: true, + devices: [] + }, + force: true, + immediate: false }); + // TODO prevent firing when page is loaded + const { data: deviceData, send: readDeviceData } = useRequest((id) => EMSESP.readDeviceData(id), { + initialData: { + data: [] + }, + force: true, + immediate: false + }); + + // TODO prevent firing when page is loaded + const { loading: submitting, send: writeDeviceValue } = useRequest( + (id: number, deviceValue: DeviceValue) => EMSESP.writeDeviceValue(id, deviceValue), + { + immediate: false + } + ); + useLayoutEffect(() => { function updateSize() { setSize([window.innerWidth, window.innerHeight]); @@ -212,19 +241,21 @@ const DashboardDevices: FC = () => { } ); - const fetchDeviceData = async (id: number) => { - try { - setDeviceData((await EMSESP.readDeviceData({ id })).data); - } catch (error) { - toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING())); - } - }; + // TODO remove + // const fetchDeviceData = async (id: number) => { + // try { + // setDeviceData((await EMSESP.readDeviceData({ id })).data); + // } catch (error) { + // toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING())); + // } + // }; - function onSelectChange(action: any, state: any) { - setDeviceData({ data: [] }); + async function onSelectChange(action: any, state: any) { + // TODO check if still needed + // setDeviceData({ data: [] }); setSelectedDevice(state.id); if (action.type === 'ADD_BY_ID_EXCLUSIVELY') { - void fetchDeviceData(state.id); + await readDeviceData(state.id); } } @@ -257,27 +288,29 @@ const DashboardDevices: FC = () => { }; }, [escFunction]); - const fetchCoreData = useCallback(async () => { - try { - setSelectedDevice(undefined); - setCoreData((await EMSESP.readCoreData()).data); - } catch (error) { - toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING())); - } - }, [LL]); + // TODO remove + // const fetchCoreData = useCallback(async () => { + // try { + // setSelectedDevice(undefined); + // setCoreData((await EMSESP.readCoreData()).data); + // } catch (error) { + // toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING())); + // } + // }, [LL]); - useEffect(() => { - void fetchCoreData(); - }, [fetchCoreData]); + // TODO remove + // useEffect(() => { + // void fetchCoreData2(); + // }, [fetchCoreData2]); const refreshData = () => { if (deviceValueDialogOpen) { return; } if (selectedDevice) { - void fetchDeviceData(selectedDevice); + void readDeviceData(selectedDevice); } else { - void fetchCoreData(); + void readCoreData(); } }; @@ -348,25 +381,32 @@ const DashboardDevices: FC = () => { const deviceValueDialogSave = async (dv: DeviceValue) => { const selectedDeviceID = Number(device_select.state.id); - try { - const response = await EMSESP.writeDeviceValue({ - id: selectedDeviceID, - devicevalue: dv - }); - if (response.status === 204) { - toast.error(LL.WRITE_CMD_FAILED()); - } else if (response.status === 403) { - toast.error(LL.ACCESS_DENIED()); - } else { - toast.success(LL.WRITE_CMD_SENT()); - } - } catch (error) { - toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); - } finally { - setDeviceValueDialogOpen(false); - await fetchDeviceData(selectedDeviceID); - setSelectedDeviceValue(undefined); - } + // TODO For all Push, do error handling? + const response = await writeDeviceValue(selectedDeviceID, dv); + console.log(response); + setDeviceValueDialogOpen(false); + await readDeviceData(selectedDeviceID); + setSelectedDeviceValue(undefined); + + // try { + // const response = await EMSESP.writeDeviceValue({ + // id: selectedDeviceID, + // devicevalue: dv + // }); + // if (response.status === 204) { + // toast.error(LL.WRITE_CMD_FAILED()); + // } else if (response.status === 403) { + // toast.error(LL.ACCESS_DENIED()); + // } else { + // toast.success(LL.WRITE_CMD_SENT()); + // } + // } catch (error) { + // toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); + // } finally { + // setDeviceValueDialogOpen(false); + // await readDeviceData(selectedDeviceID); + // setSelectedDeviceValue(undefined); + // } }; const renderDeviceDetails = () => { @@ -457,7 +497,7 @@ const DashboardDevices: FC = () => { }; const renderDeviceData = () => { - if (!selectedDevice) { + if (!selectedDevice || deviceData.data === undefined) { return; } @@ -612,6 +652,7 @@ const DashboardDevices: FC = () => { !hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY) } validator={deviceValueItemValidation(selectedDeviceValue)} + progress={submitting} /> )} diff --git a/interface/src/project/DashboardDevicesDialog.tsx b/interface/src/project/DashboardDevicesDialog.tsx index f2ff2bc33..bc725f0a6 100644 --- a/interface/src/project/DashboardDevicesDialog.tsx +++ b/interface/src/project/DashboardDevicesDialog.tsx @@ -13,8 +13,10 @@ import { FormHelperText, Grid, Box, - Typography + Typography, + CircularProgress } from '@mui/material'; +import { green } from '@mui/material/colors'; import { useState, useEffect } from 'react'; import { DeviceValueUOM, DeviceValueUOM_s } from './types'; @@ -22,7 +24,7 @@ import type { DeviceValue } from './types'; import type Schema from 'async-validator'; import type { ValidateFieldsError } from 'async-validator'; -import { ValidatedTextField } from 'components'; +import { ButtonRow, ValidatedTextField } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; import { updateValue } from 'utils'; @@ -35,6 +37,7 @@ type DashboardDevicesDialogProps = { selectedItem: DeviceValue; writeable: boolean; validator: Schema; + progress: boolean; }; const DashboarDevicesDialog = ({ @@ -43,7 +46,8 @@ const DashboarDevicesDialog = ({ onSave, selectedItem, writeable, - validator + validator, + progress }: DashboardDevicesDialogProps) => { const { LL } = useI18nContext(); const [editItem, setEditItem] = useState(selectedItem); @@ -184,14 +188,32 @@ const DashboarDevicesDialog = ({ {writeable ? ( - <> + - + {progress && ( + + )} + ) : (