mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 00:09:51 +03:00
Merge remote-tracking branch 'origin/v3.4' into dev
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
type UpdateEntity<S> = (state: (prevState: Readonly<S>) => S) => void;
|
||||
|
||||
export const extractEventValue = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
export const numberValue = (value: number) => (isNaN(value) ? '' : value.toString());
|
||||
|
||||
export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
switch (event.target.type) {
|
||||
case 'number':
|
||||
return event.target.valueAsNumber;
|
||||
@@ -13,21 +13,11 @@ export const extractEventValue = (
|
||||
}
|
||||
};
|
||||
|
||||
export const updateValue = <S>(updateEntity: UpdateEntity<S>) => (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
updateEntity((prevState) => ({
|
||||
...prevState,
|
||||
[event.target.name]: extractEventValue(event)
|
||||
}));
|
||||
};
|
||||
|
||||
export const updateBooleanValue = <S>(updateEntity: UpdateEntity<S>) => (
|
||||
name: string,
|
||||
value?: boolean
|
||||
) => {
|
||||
updateEntity((prevState) => ({
|
||||
...prevState,
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
export const updateValue =
|
||||
<S>(updateEntity: UpdateEntity<S>) =>
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateEntity((prevState) => ({
|
||||
...prevState,
|
||||
[event.target.name]: extractEventValue(event)
|
||||
}));
|
||||
};
|
||||
|
||||
4
interface/src/utils/endpoints.ts
Normal file
4
interface/src/utils/endpoints.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
export const extractErrorMessage = (error: AxiosError, defaultMessage: string) =>
|
||||
(error.response && error.response.data ? error.response.data.message : error.message) || defaultMessage;
|
||||
@@ -1 +1,6 @@
|
||||
export * from './binding';
|
||||
export * from './endpoints';
|
||||
export * from './route';
|
||||
export * from './submit';
|
||||
export * from './time';
|
||||
export * from './useRest';
|
||||
|
||||
1
interface/src/utils/route.ts
Normal file
1
interface/src/utils/route.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const routeMatches = (route: string, pathname: string) => pathname.startsWith(route + '/') || pathname === route;
|
||||
8
interface/src/utils/submit.ts
Normal file
8
interface/src/utils/submit.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const onEnterCallback =
|
||||
(callback: () => void): ((event: React.KeyboardEvent) => void) =>
|
||||
(event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
callback();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
40
interface/src/utils/time.ts
Normal file
40
interface/src/utils/time.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import parseMilliseconds from 'parse-ms';
|
||||
|
||||
const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
hour12: false
|
||||
});
|
||||
|
||||
export const formatDateTime = (dateTime: string) => {
|
||||
return LOCALE_FORMAT.format(new Date(dateTime.substr(0, 19)));
|
||||
};
|
||||
|
||||
export const formatLocalDateTime = (date: Date) => {
|
||||
return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, -1).substr(0, 19);
|
||||
};
|
||||
|
||||
export const formatDuration = (duration: number) => {
|
||||
const { days, hours, minutes, seconds } = parseMilliseconds(duration * 1000);
|
||||
var formatted = '';
|
||||
if (days) {
|
||||
formatted += pluralize(days, 'day');
|
||||
}
|
||||
if (formatted || hours) {
|
||||
formatted += pluralize(hours, 'hour');
|
||||
}
|
||||
if (formatted || minutes) {
|
||||
formatted += pluralize(minutes, 'minute');
|
||||
}
|
||||
if (formatted || seconds) {
|
||||
formatted += pluralize(seconds, 'second');
|
||||
}
|
||||
return formatted;
|
||||
};
|
||||
|
||||
const pluralize = (count: number, noun: string, suffix: string = 's') =>
|
||||
` ${count} ${noun}${count !== 1 ? suffix : ''} `;
|
||||
66
interface/src/utils/useRest.ts
Normal file
66
interface/src/utils/useRest.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { AxiosPromise } from 'axios';
|
||||
|
||||
import { extractErrorMessage } from '.';
|
||||
|
||||
export interface RestRequestOptions<D> {
|
||||
read: () => AxiosPromise<D>;
|
||||
update?: (value: D) => AxiosPromise<D>;
|
||||
}
|
||||
|
||||
export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [saving, setSaving] = useState<boolean>(false);
|
||||
const [data, setData] = useState<D>();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setData(undefined);
|
||||
setErrorMessage(undefined);
|
||||
try {
|
||||
setData((await read()).data);
|
||||
} catch (error: any) {
|
||||
const message = extractErrorMessage(error, 'Problem loading data');
|
||||
enqueueSnackbar(message, { variant: 'error' });
|
||||
setErrorMessage(message);
|
||||
}
|
||||
}, [read, enqueueSnackbar]);
|
||||
|
||||
const save = useCallback(
|
||||
async (toSave: D) => {
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
setRestartNeeded(false);
|
||||
setErrorMessage(undefined);
|
||||
try {
|
||||
const response = await update(toSave);
|
||||
setData(response.data);
|
||||
if (response.status === 202) {
|
||||
setRestartNeeded(true);
|
||||
} else {
|
||||
enqueueSnackbar('Settings saved', { variant: 'success' });
|
||||
}
|
||||
} catch (error: any) {
|
||||
const message = extractErrorMessage(error, 'Problem saving data');
|
||||
enqueueSnackbar(message, { variant: 'error' });
|
||||
setErrorMessage(message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
},
|
||||
[update, enqueueSnackbar]
|
||||
);
|
||||
|
||||
const saveData = () => data && save(data);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
return { loadData, saveData, saving, setData, data, errorMessage, restartNeeded } as const;
|
||||
};
|
||||
94
interface/src/utils/useWs.ts
Normal file
94
interface/src/utils/useWs.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import Sockette from 'sockette';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { addAccessTokenParameter } from '../api/authentication';
|
||||
|
||||
interface WebSocketIdMessage {
|
||||
type: 'id';
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface WebSocketPayloadMessage<D> {
|
||||
type: 'payload';
|
||||
origin_id: string;
|
||||
payload: D;
|
||||
}
|
||||
|
||||
export type WebSocketMessage<D> = WebSocketIdMessage | WebSocketPayloadMessage<D>;
|
||||
|
||||
export const useWs = <D>(wsUrl: string, wsThrottle: number = 100) => {
|
||||
const ws = useRef<Sockette>();
|
||||
const clientId = useRef<string>();
|
||||
|
||||
const [connected, setConnected] = useState<boolean>(false);
|
||||
const [data, setData] = useState<D>();
|
||||
const [transmit, setTransmit] = useState<boolean>();
|
||||
const [clear, setClear] = useState<boolean>();
|
||||
|
||||
const onMessage = useCallback((event: MessageEvent) => {
|
||||
const rawData = event.data;
|
||||
if (typeof rawData === 'string' || rawData instanceof String) {
|
||||
const message = JSON.parse(rawData as string) as WebSocketMessage<D>;
|
||||
switch (message.type) {
|
||||
case 'id':
|
||||
clientId.current = message.id;
|
||||
break;
|
||||
case 'payload':
|
||||
if (clientId.current) {
|
||||
setData((existingData) => (clientId.current === message.origin_id && existingData) || message.payload);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const doSaveData = useCallback((newData: D, clearData: boolean = false) => {
|
||||
if (!ws.current) {
|
||||
return;
|
||||
}
|
||||
if (clearData) {
|
||||
setData(undefined);
|
||||
}
|
||||
ws.current.json(newData);
|
||||
}, []);
|
||||
|
||||
const saveData = useRef(debounce(doSaveData, wsThrottle));
|
||||
|
||||
const updateData = (
|
||||
newData: React.SetStateAction<D | undefined>,
|
||||
transmitData: boolean = true,
|
||||
clearData: boolean = false
|
||||
) => {
|
||||
setData(newData);
|
||||
setTransmit(transmitData);
|
||||
setClear(clearData);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!transmit) {
|
||||
return;
|
||||
}
|
||||
data && saveData.current(data, clear);
|
||||
setTransmit(false);
|
||||
setClear(false);
|
||||
}, [doSaveData, data, transmit, clear]);
|
||||
|
||||
useEffect(() => {
|
||||
const instance = new Sockette(addAccessTokenParameter(wsUrl), {
|
||||
onmessage: onMessage,
|
||||
onopen: () => {
|
||||
setConnected(true);
|
||||
},
|
||||
onclose: () => {
|
||||
clientId.current = undefined;
|
||||
setConnected(false);
|
||||
setData(undefined);
|
||||
}
|
||||
});
|
||||
ws.current = instance;
|
||||
return instance.close;
|
||||
}, [wsUrl, onMessage]);
|
||||
|
||||
return { connected, data, updateData } as const;
|
||||
};
|
||||
Reference in New Issue
Block a user