diff --git a/ui/package-lock.json b/ui/package-lock.json index 91bf699..e1f11f2 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -9,6 +9,7 @@ "version": "1.1.0", "dependencies": { "@heroicons/react": "^2.0.16", + "@radix-ui/react-alert-dialog": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.2", "@radix-ui/react-select": "^1.2.1", "@radix-ui/react-slider": "^1.1.1", @@ -1107,6 +1108,24 @@ "@babel/runtime": "^7.13.10" } }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.3.tgz", + "integrity": "sha512-QXFy7+bhGi0u+paF2QbJeSCHZs4gLMJIPm6sajUamyW0fro6g1CaSGc5zmc4QmK2NlSGUrq8m+UsUqJYtzvXow==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dialog": "1.0.3", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-slot": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.2.tgz", @@ -1178,6 +1197,32 @@ "react": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.3.tgz", + "integrity": "sha512-owNhq36kNPqC2/a+zJRioPg6HHnTn5B/sh/NjTY8r4W9g1L5VJlrzZIVcBr7R9Mg8iLjVmh6MGgMlfoVf/WO/A==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.3", + "@radix-ui/react-focus-guards": "1.0.0", + "@radix-ui/react-focus-scope": "1.0.2", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-portal": "1.0.2", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-slot": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.0.tgz", @@ -10733,6 +10778,20 @@ "@babel/runtime": "^7.13.10" } }, + "@radix-ui/react-alert-dialog": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.3.tgz", + "integrity": "sha512-QXFy7+bhGi0u+paF2QbJeSCHZs4gLMJIPm6sajUamyW0fro6g1CaSGc5zmc4QmK2NlSGUrq8m+UsUqJYtzvXow==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dialog": "1.0.3", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-slot": "1.0.1" + } + }, "@radix-ui/react-arrow": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.2.tgz", @@ -10786,6 +10845,28 @@ "@babel/runtime": "^7.13.10" } }, + "@radix-ui/react-dialog": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.3.tgz", + "integrity": "sha512-owNhq36kNPqC2/a+zJRioPg6HHnTn5B/sh/NjTY8r4W9g1L5VJlrzZIVcBr7R9Mg8iLjVmh6MGgMlfoVf/WO/A==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.3", + "@radix-ui/react-focus-guards": "1.0.0", + "@radix-ui/react-focus-scope": "1.0.2", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-portal": "1.0.2", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-slot": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + } + }, "@radix-ui/react-direction": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.0.tgz", diff --git a/ui/package.json b/ui/package.json index 04155eb..f866450 100644 --- a/ui/package.json +++ b/ui/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@heroicons/react": "^2.0.16", + "@radix-ui/react-alert-dialog": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.2", "@radix-ui/react-select": "^1.2.1", "@radix-ui/react-slider": "^1.1.1", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 8501522..ed83f6a 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -2,6 +2,7 @@ lockfileVersion: 5.4 specifiers: '@heroicons/react': ^2.0.16 + '@radix-ui/react-alert-dialog': ^1.0.3 '@radix-ui/react-collapsible': ^1.0.2 '@radix-ui/react-select': ^1.2.1 '@radix-ui/react-slider': ^1.1.1 @@ -55,6 +56,7 @@ specifiers: dependencies: '@heroicons/react': 2.0.16_react@18.2.0 + '@radix-ui/react-alert-dialog': 1.0.3_zula6vjvt3wdocc4mwcxqa6nzi '@radix-ui/react-collapsible': 1.0.2_biqbaboplfbrettd7655fr4n2y '@radix-ui/react-select': 1.2.1_zula6vjvt3wdocc4mwcxqa6nzi '@radix-ui/react-slider': 1.1.1_biqbaboplfbrettd7655fr4n2y @@ -869,6 +871,25 @@ packages: '@babel/runtime': 7.20.7 dev: false + /@radix-ui/react-alert-dialog/1.0.3_zula6vjvt3wdocc4mwcxqa6nzi: + resolution: {integrity: sha512-QXFy7+bhGi0u+paF2QbJeSCHZs4gLMJIPm6sajUamyW0fro6g1CaSGc5zmc4QmK2NlSGUrq8m+UsUqJYtzvXow==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.20.7 + '@radix-ui/primitive': 1.0.0 + '@radix-ui/react-compose-refs': 1.0.0_react@18.2.0 + '@radix-ui/react-context': 1.0.0_react@18.2.0 + '@radix-ui/react-dialog': 1.0.3_zula6vjvt3wdocc4mwcxqa6nzi + '@radix-ui/react-primitive': 1.0.2_biqbaboplfbrettd7655fr4n2y + '@radix-ui/react-slot': 1.0.1_react@18.2.0 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + transitivePeerDependencies: + - '@types/react' + dev: false + /@radix-ui/react-arrow/1.0.2_biqbaboplfbrettd7655fr4n2y: resolution: {integrity: sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==} peerDependencies: @@ -933,6 +954,33 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-dialog/1.0.3_zula6vjvt3wdocc4mwcxqa6nzi: + resolution: {integrity: sha512-owNhq36kNPqC2/a+zJRioPg6HHnTn5B/sh/NjTY8r4W9g1L5VJlrzZIVcBr7R9Mg8iLjVmh6MGgMlfoVf/WO/A==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.20.7 + '@radix-ui/primitive': 1.0.0 + '@radix-ui/react-compose-refs': 1.0.0_react@18.2.0 + '@radix-ui/react-context': 1.0.0_react@18.2.0 + '@radix-ui/react-dismissable-layer': 1.0.3_biqbaboplfbrettd7655fr4n2y + '@radix-ui/react-focus-guards': 1.0.0_react@18.2.0 + '@radix-ui/react-focus-scope': 1.0.2_biqbaboplfbrettd7655fr4n2y + '@radix-ui/react-id': 1.0.0_react@18.2.0 + '@radix-ui/react-portal': 1.0.2_biqbaboplfbrettd7655fr4n2y + '@radix-ui/react-presence': 1.0.0_biqbaboplfbrettd7655fr4n2y + '@radix-ui/react-primitive': 1.0.2_biqbaboplfbrettd7655fr4n2y + '@radix-ui/react-slot': 1.0.1_react@18.2.0 + '@radix-ui/react-use-controllable-state': 1.0.0_react@18.2.0 + aria-hidden: 1.2.1_pmekkgnqduwlme35zpnqhenc34 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-remove-scroll: 2.5.5_pmekkgnqduwlme35zpnqhenc34 + transitivePeerDependencies: + - '@types/react' + dev: false + /@radix-ui/react-direction/1.0.0_react@18.2.0: resolution: {integrity: sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==} peerDependencies: diff --git a/ui/public/locales/de/translation.json b/ui/public/locales/de/translation.json index 49dedc6..1c58d55 100644 --- a/ui/public/locales/de/translation.json +++ b/ui/public/locales/de/translation.json @@ -1,4 +1,8 @@ { + "alertDialog": { + "confirm": "Bestätigen", + "cancel": "Schließen" + }, "customAnimations": { "deleteSuccessful": "Erfolgreich gelöscht!", "file": "Datei", @@ -87,6 +91,12 @@ "warning": "Warnung", "error": "Fehler" }, + "motionSensor": "Bewegungssensor", + "motionSensorCalibration": "Kalibrierung", + "motionSensorAutoCalibration": "Starte Auto-Kalibrierung", + "motionSensorAutoCalibrationTitle": "Möchtest du die automatische Kalibrierung des Bewegungssensors starten?", + "motionSensorAutoCalibrationDescription": "Stelle sicher, dass das Fahrzeug geparkt ist und die Innentemperatur zwischen -10°C und 50°C beträgt.", + "motionSensorAutoCalibrationSuccessful": "Erfolgreich kalibriert.", "power": "Power", "regulator": "Regler", "regulatorCutoffTemperature": "Abschalttemperatur (°C)", diff --git a/ui/public/locales/en/translation.json b/ui/public/locales/en/translation.json index 7c71aeb..4316c71 100644 --- a/ui/public/locales/en/translation.json +++ b/ui/public/locales/en/translation.json @@ -1,4 +1,8 @@ { + "alertDialog": { + "confirm": "Confirm", + "close": "Close" + }, "customAnimations": { "deleteSuccessful": "Successfully deleted!", "file": "File", @@ -87,6 +91,12 @@ "info": "Info", "warning": "Warning" }, + "motionSensor": "Motion Sensor", + "motionSensorCalibration": "Calibration", + "motionSensorAutoCalibration": "Start Auto Calibration", + "motionSensorAutoCalibrationTitle": "Do you want to start the automatic calibration of the motion sensor?", + "motionSensorAutoCalibrationDescription": "Make sure that the car is parked and the interior temperature is between -10°C and 50°C.", + "motionSensorAutoCalibrationSuccessful": "Successfully calibrated.", "power": "Power", "regulator": "Regulator", "regulatorCutoffTemperature": "Shut Down Temperature (°C)", diff --git a/ui/src/components/AlertDialog.tsx b/ui/src/components/AlertDialog.tsx new file mode 100644 index 0000000..6e4afed --- /dev/null +++ b/ui/src/components/AlertDialog.tsx @@ -0,0 +1,78 @@ +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; +import cx from 'classnames'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +type AlertDialogProps = Omit< + AlertDialogPrimitive.AlertDialogProps, + 'open' | 'onOpenChange' +> & { + title: React.ReactNode; + description: React.ReactNode; + onConfirm: () => void; +}; + +export const AlertDialog = ({ + children, + title, + description, + onConfirm, + ...props +}: AlertDialogProps): JSX.Element => { + const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + + return ( + + + {children} + + + + + + + {title} + + + {description} + +
+ + {t('alertDialog.close')} + + + {t('alertDialog.confirm')} + +
+
+
+
+ ); +}; diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx index 54540b2..3c04d68 100644 --- a/ui/src/components/Button.tsx +++ b/ui/src/components/Button.tsx @@ -1,30 +1,32 @@ +import { forwardRef } from 'react'; import { twMerge } from 'tailwind-merge'; -type ButtonProps = { +export type ButtonProps = { variant?: 'primary' | 'secondary'; size?: 'small'; } & React.ButtonHTMLAttributes; -export const Button = ({ - children, - className, - variant, - size, - ...props -}: ButtonProps) => ( - +const Button = forwardRef( + ({ children, className, variant, size, ...props }, forwardedRef) => ( + + ), ); + +Button.displayName = 'Button'; + +export { Button }; diff --git a/ui/src/components/Collapsible.tsx b/ui/src/components/Collapsible.tsx index 1479c0d..f496f36 100644 --- a/ui/src/components/Collapsible.tsx +++ b/ui/src/components/Collapsible.tsx @@ -49,5 +49,3 @@ export const Collapsible = ({ ); }; - -export default Collapsible; diff --git a/ui/src/components/Toast.tsx b/ui/src/components/Toast.tsx index f6024f1..e191a96 100644 --- a/ui/src/components/Toast.tsx +++ b/ui/src/components/Toast.tsx @@ -19,7 +19,7 @@ export const Toast = ({ -
+
{t('toast.close')} diff --git a/ui/src/components/index.tsx b/ui/src/components/index.tsx index a4dfa62..51e6ef0 100644 --- a/ui/src/components/index.tsx +++ b/ui/src/components/index.tsx @@ -1,3 +1,4 @@ +export * from './AlertDialog'; export * from './Animation'; export * from './Button'; export * from './Collapsible'; diff --git a/ui/src/mocks/handlers.ts b/ui/src/mocks/handlers.ts index 3ed623d..ce247c8 100644 --- a/ui/src/mocks/handlers.ts +++ b/ui/src/mocks/handlers.ts @@ -2,6 +2,7 @@ import { rest } from 'msw'; import type { Fseq, Led, + MotionSensorCalibration, Profile, Profiles, System, @@ -36,21 +37,7 @@ const mock: { system: SystemInfo; }; motion: { - // TODO: Update type - motionSensorCalibration: { - accXRaw: number; - accYRaw: number; - accZRaw: number; - gyroXRaw: number; - gyroYRaw: number; - gyroZRaw: number; - accXG: number; - accYG: number; - accZG: number; - gyroXDeg: number; - gyroYDeg: number; - gyroZDeg: number; - }; + motionSensorCalibration: MotionSensorCalibration; }; fseq: { fileList: Fseq[]; @@ -358,7 +345,10 @@ export const handlers = [ rest.patch('/api/config/motion', async (_req, res, ctx) => { console.debug(`Patch motion configuration: ${mock.motion}`); - return res(ctx.status(200), ctx.json({ status: 200, message: 'ok' })); + return throttledRes( + ctx.status(200), + ctx.json({ status: 200, message: 'ok' }), + ); }), // --------------- diff --git a/ui/src/pages/Settings.tsx b/ui/src/pages/Settings.tsx index 5dd2304..6b8f34a 100644 --- a/ui/src/pages/Settings.tsx +++ b/ui/src/pages/Settings.tsx @@ -4,6 +4,7 @@ import { ErrorBoundary } from 'react-error-boundary'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { + AlertDialog, Button, Error, InputPassword, @@ -19,6 +20,7 @@ import { import i18n from '../i18n'; import { changeTheme, toPercentage } from '../libs'; import { + useAutoMotionSensorCalibration, useSystem, useSystemInfo, useUi, @@ -134,6 +136,13 @@ const Form = (): JSX.Element => { isError: isUiError, error: uiError, } = useUpdateUi(); + const { + mutateAsync: mutateAsyncAutoMotionSensorCalibration, + isSuccess: isAutoMotionSensorCalibrationSuccess, + isLoading: isAutoMotionSensorCalibrationLoading, + isError: isAutoMotionSensorCalibrationError, + error: autoMotionSensorCalibrationError, + } = useAutoMotionSensorCalibration(); const { handleSubmit, @@ -262,6 +271,10 @@ const Form = (): JSX.Element => { )} + {isAutoMotionSensorCalibrationSuccess && ( + + )} + {isSystemError && ( )} @@ -272,6 +285,15 @@ const Form = (): JSX.Element => { {isUiError && } + {isAutoMotionSensorCalibrationError && ( + + )} + + {isAutoMotionSensorCalibrationLoading && } + {Number(values.system.fanMode) === FanMode.Automatic && !hasTemperatureSensors && ( { )} + {(systemInfo?.hardwareInfo.mpu6050 ?? 0) > 0 && ( +
+ + {t('settings.motionSensor')} + + + +
+ )} +
{t('settings.ui')} diff --git a/ui/src/pages/api/index.ts b/ui/src/pages/api/index.ts index c27245d..8fc264d 100644 --- a/ui/src/pages/api/index.ts +++ b/ui/src/pages/api/index.ts @@ -1,6 +1,7 @@ export * from './fseq'; export * from './led'; export * from './log'; +export * from './motionSensorCalibration'; export * from './profile'; export * from './system'; export * from './systemInfo'; diff --git a/ui/src/pages/api/motionSensorCalibration.ts b/ui/src/pages/api/motionSensorCalibration.ts new file mode 100644 index 0000000..bc0a6bb --- /dev/null +++ b/ui/src/pages/api/motionSensorCalibration.ts @@ -0,0 +1,30 @@ +import { useMutation } from '@tanstack/react-query'; +import ky from 'ky'; + +export const MOTION_SENSOR_CALIBRATION_API_URL = '/api/config/motion'; + +export type MotionSensorCalibration = { + accXRaw: number; + accYRaw: number; + accZRaw: number; + gyroXRaw: number; + gyroYRaw: number; + gyroZRaw: number; + accXG: number; + accYG: number; + accZG: number; + gyroXDeg: number; + gyroYDeg: number; + gyroZDeg: number; +}; + +type Response = { + status: number; + message: string; +}; + +export const useAutoMotionSensorCalibration = () => + useMutation({ + mutationFn: async () => + await ky.patch(`${MOTION_SENSOR_CALIBRATION_API_URL}`).json(), + });