diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..55371e5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +.vscode \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index b58b603..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 03d9549..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 758e23c..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/thinker-react.iml b/.idea/thinker-react.iml deleted file mode 100644 index 0c8867d..0000000 --- a/.idea/thinker-react.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bcb9480 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM node:16 as build-stage + +WORKDIR /app + +COPY package*.json /app/ + +RUN npm ci --only=production + +COPY ./ /app/ + +RUN npm run build + + +FROM nginx:1.21 + +COPY --from=build-stage /app/build/ /usr/share/nginx/html + +COPY --from=build-stage /nginx.conf /etc/nginx/conf.d/default.conf \ No newline at end of file diff --git a/package.json b/package.json index ce12c4e..d10c27a 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,5 @@ "last 1 firefox version", "last 1 safari version" ] - }, - "proxy": "http://thinker.local:8080/" + } } diff --git a/src/App.js b/src/App.js index 4ef8e24..d665bed 100644 --- a/src/App.js +++ b/src/App.js @@ -2,6 +2,9 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { Main } from "./pages/Main"; import Home from "./pages/Home"; import { ConnectPage } from "./pages/ConnectPage"; +import { SettingsPage } from "pages/settings/SettingsPage"; +import { HooksPage } from "pages/hooks/HooksPage"; +import { AddDeviceTriggersPage } from "pages/AddDeviceTriggersPage"; function App() { return ( @@ -10,6 +13,9 @@ function App() { }> } /> } /> + } /> + } /> + } /> diff --git a/src/api/ThinkerApi.js b/src/api/ThinkerApi.js index 5c01e3a..cba95ba 100644 --- a/src/api/ThinkerApi.js +++ b/src/api/ThinkerApi.js @@ -1,6 +1,7 @@ import canNdjsonStream from "can-ndjson-stream"; +import { BACKEND_URL } from "./contants"; -export const BASE_URL = "/api" +export const BASE_URL = `${BACKEND_URL}/api` export const buildApiUrl = (url, options) => { let apiUrl = BASE_URL + url; diff --git a/src/api/contants.js b/src/api/contants.js index 349df6e..9acff63 100644 --- a/src/api/contants.js +++ b/src/api/contants.js @@ -1,7 +1,21 @@ +export const BACKEND_URL = "http://thinker.local:8080" +export const DEVICE_STATUS_WAITING_CONFIGURATION = "WAITING_CONFIGURATION"; export const DEVICE_NAME_MIN_LENGTH = 4; export const DEVICE_NAME_LENGTH = 256; export const DEVICE_DESCRIPTION_LENGTH = 2048; export const DEVICE_CLASS_MAX_LENGTH = 256; -export const DEVICE_REPORT_TYPES_MIN_LENGTH = 1; \ No newline at end of file +export const DEVICE_REPORT_TYPES_MIN_LENGTH = 1; + +export const WIFI_PASSWORD_MIN_LENGTH = 8 +export const WIFI_PASSWORD_MAX_LENGTH = 256 +export const WIFI_SSID_MIN_LENGTH = 2 +export const WIFI_SSID_MAX_LENGTH = 256 + +export const EMAIL_MIN_LENGTH = 6 +export const EMAIL_MAX_LENGTH = 256 +export const EMAIL_PASSWORD_MIN_LENGTH = 2 +export const EMAIL_PASSWORD_MAX_LENGTH = 256 + +export const HOOK_TYPE_SEND_EMAIL = "send_email" \ No newline at end of file diff --git a/src/api/services/appSettingsApi.js b/src/api/services/appSettingsApi.js new file mode 100644 index 0000000..136c254 --- /dev/null +++ b/src/api/services/appSettingsApi.js @@ -0,0 +1,36 @@ +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react"; +import { BACKEND_URL } from "api/contants"; + +export const APP_SETTINGS_TYPE = "APPLICATION"; +export const MAIL_SETTINGS_TYPE = "MAIL"; + +export const appSettingsApi = createApi({ + reducerPath: "appSettingsApi", + baseQuery: fetchBaseQuery({ baseUrl: `${BACKEND_URL}/api/settings` }), + endpoints: builder => ({ + getSettingsStatus: builder.query({ + query: () => ({ method: "GET", url: "/status" }) + }), + getSettingsByType: builder.query({ + query: ({type}) => ({ method: "GET", url: `/${type}` }) + }), + getAppSettings: builder.query({ + query: () => ({ method: "GET", url: "" }) + }), + updateMailSettings: builder.mutation({ + query: ({...data }) => ({ method: "POST", url: "/mail", body: { ...data } }) + }), + updateAppSettings: builder.mutation({ + query: ({...data }) => ({ method: "POST", url: "", body: { ...data } }) + }) + }) + }); + + export const { + useGetSettingsStatusQuery, + useGetAppSettingsQuery, + useGetSettingsByTypeQuery, + useUpdateMailSettingsMutation, + useUpdateAppSettingsMutation + } = appSettingsApi + diff --git a/src/api/services/devicesApi.js b/src/api/services/devicesApi.js index 55baa47..d231ac6 100644 --- a/src/api/services/devicesApi.js +++ b/src/api/services/devicesApi.js @@ -1,15 +1,16 @@ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react"; +import { BACKEND_URL } from "api/contants"; export const devicesApi = createApi({ reducerPath: "devicesApi", - baseQuery: fetchBaseQuery({ baseUrl: "/api/devices/" }), + baseQuery: fetchBaseQuery({ baseUrl: `${BACKEND_URL}/api/devices` }), endpoints: builder => ({ getReportsPage: builder.query({ query: ({ deviceId, page, pageSize }) => ({ url: `${deviceId}/reports?page=${page}&pageSize=${pageSize}` }) }), executeAction: builder.mutation({ - query: (deviceId, actionName) => ({ method: "POST", url: `${deviceId}/${actionName}` }) + query: ({deviceId, actionName}) => ({ method: "POST", url: `${deviceId}/${actionName}` }) }), patchDevice: builder.mutation({ query: ({ deviceId, ...data }) => ({ method: "PATCH", url: `${deviceId}`, body: { ...data } }) diff --git a/src/api/services/discoveryApi.js b/src/api/services/discoveryApi.js index ec02bd2..7991068 100644 --- a/src/api/services/discoveryApi.js +++ b/src/api/services/discoveryApi.js @@ -1,8 +1,9 @@ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react"; +import { BACKEND_URL } from "api/contants"; export const discoveryApi = createApi({ reducerPath: "discoveryApi", - baseQuery: fetchBaseQuery({ baseUrl: "/api/discovery" }), + baseQuery: fetchBaseQuery({ baseUrl: `${BACKEND_URL}/api/discovery` }), endpoints: builder => ({ getDiscoveryStatus: builder.query({ query: () => ({ url: "/status" }), diff --git a/src/api/services/hooksApi.js b/src/api/services/hooksApi.js new file mode 100644 index 0000000..a67694a --- /dev/null +++ b/src/api/services/hooksApi.js @@ -0,0 +1,37 @@ +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react"; +import { BACKEND_URL } from "api/contants"; + +export const hooksApi = createApi({ + reducerPath: "hooksApi", + baseQuery: fetchBaseQuery({ baseUrl: `${BACKEND_URL}/api` }), + endpoints: builder => ({ + getAllHooks: builder.query({ + query: () => ({ url: "/hooks" }) + }), + createEmailHook: builder.mutation({ + query: ({...hook}) => ({ method: "POST", url: "/hooks/sendEmail", body: {...hook}}) + }), + deleteHook: builder.mutation({ + query: (id) => ({ method: "DELETE", url: `/hooks/${id}`}) + }), + getDeviceTriggers: builder.query({ + query: (deviceId) => ({ url: `/triggers/${deviceId}`}) + }), + createTriggers: builder.mutation({ + query: ({deviceId, ...triggersAndHooks}) => ({ method: "POST", url: `/triggers/${deviceId}`, body: {...triggersAndHooks}}) + }), + deleteTrigger: builder.mutation({ + query: (id) => ({ method: "DELETE", url: `/triggers/${id}`}) + }), + }) +}); + + +export const { + useGetAllHooksQuery, + useCreateEmailHookMutation, + useGetDeviceTriggersQuery, + useCreateTriggersMutation, + useDeleteHookMutation, + useDeleteTriggerMutation +} = hooksApi \ No newline at end of file diff --git a/src/components/button/ActionButton.js b/src/components/button/ActionButton.js index c0cf884..90d9f22 100644 --- a/src/components/button/ActionButton.js +++ b/src/components/button/ActionButton.js @@ -2,7 +2,7 @@ import { IconButton } from "@chakra-ui/react"; const ActionButton = props => { return ( - + {props.children} ); diff --git a/src/components/button/DeleteButton.js b/src/components/button/DeleteButton.js new file mode 100644 index 0000000..c221cd0 --- /dev/null +++ b/src/components/button/DeleteButton.js @@ -0,0 +1,7 @@ +import { CloseIcon } from "@chakra-ui/icons"; +import { ActionButton } from "components/button/ActionButton"; +import { ChakraIcon } from "components/icon/ChakraIcon"; + +export const DeleteButton = props => { + return } {...props} />; +}; diff --git a/src/components/button/PlusButton.js b/src/components/button/PlusButton.js new file mode 100644 index 0000000..9367f7a --- /dev/null +++ b/src/components/button/PlusButton.js @@ -0,0 +1,8 @@ +import { ActionButton } from "components/button/ActionButton"; +import { ChakraIcon } from "components/icon/ChakraIcon"; +import { IoAdd } from "react-icons/io5"; + +export const PlusButton = props => { + const icon = ; + return ; +}; diff --git a/src/components/button/SettingsButton.js b/src/components/button/SettingsButton.js new file mode 100644 index 0000000..664d1fd --- /dev/null +++ b/src/components/button/SettingsButton.js @@ -0,0 +1,8 @@ +import { SettingsIcon } from "@chakra-ui/icons"; +import { ActionButton } from "components/button/ActionButton"; +import { Link } from "react-router-dom"; + +export const SettingsButton = () => { + const icon = ; + return +}; diff --git a/src/components/devices/discovery/DiscoveredDeviceCard.js b/src/components/devices/discovery/DiscoveredDeviceCard.js index 6dbdc3a..a93a5c9 100644 --- a/src/components/devices/discovery/DiscoveredDeviceCard.js +++ b/src/components/devices/discovery/DiscoveredDeviceCard.js @@ -1,17 +1,28 @@ -import { Box, Button, Flex, Spacer, Text, VStack } from "@chakra-ui/react"; +import { Box, Button, Center, Flex, Spacer, Text, VStack } from "@chakra-ui/react"; import { useConnectDeviceMutation } from "api/services/discoveryApi"; +import { SpinnerContainer } from "components/spinner/SpinnerContainer"; +import { useState } from "react"; import { useBackgroundColors, useBorderColors, useTextColors } from "styles/theme/foundations/colors"; -export const DiscoveredDeviceCard = ({ id, name, address, discoveredAt, rssi, knownDevice }) => { - const [connectDevice, { isLoading }] = useConnectDeviceMutation(address); +export const DiscoveredDeviceCard = ({ id, name, address, discoveredAt, rssi, knownDevice, refresh }) => { + const [connectDevice] = useConnectDeviceMutation(address); + const widgetBorderColor = useBorderColors().widget; + const [isLoading, setLoading] = useState(false); + + if (isLoading) { + return
+ +
+ } + return ( {name ? name : "[unknown]"} - {address} - {discoveredAt} + {new Date(discoveredAt).toLocaleDateString("ru-RU", + { year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric" })} Rssi: {rssi} diff --git a/src/components/devices/discovery/DiscoveredDevices.js b/src/components/devices/discovery/DiscoveredDevices.js index aa4ac31..a723fbc 100644 --- a/src/components/devices/discovery/DiscoveredDevices.js +++ b/src/components/devices/discovery/DiscoveredDevices.js @@ -1,26 +1,40 @@ import { SimpleGrid } from "@chakra-ui/react"; import { useGetDiscoveredDevicesQuery } from "api/services/discoveryApi"; +import { useEffect, useState } from "react"; import { useFetchDevicesQuery } from "store/slice/devicesSlice"; +import { delay } from "utils/utils"; import { DiscoveredDeviceCard } from "./DiscoveredDeviceCard"; export const DiscoveredDevices = () => { - const { ids } = useFetchDevicesQuery(); - const { sorted: discoveredDevices, isFetching, isError } = useGetDiscoveredDevicesQuery(undefined, { + const [ isRefetching, setIsRefetching ] = useState(false); + const { ids, refetch: refetchConnected } = useFetchDevicesQuery(); + const { sorted: discoveredDevices, isFetching, isError, refetch, isSuccess } = useGetDiscoveredDevicesQuery(undefined, { + selectFromResult: result => ({ ...result, sorted: [...result.data] .filter(device => !ids.includes(device.address)) .sort((a, b) => { - return b.name.localeCompare(a) + return b.knownDevice - a.knownDevice + b.name.localeCompare(a) }) }) }); + useEffect(() => { + if (discoveredDevices.length === 0 && isSuccess && !isRefetching) { + setIsRefetching(true) + delay(1, () => { + refetch(); + setIsRefetching(false); + }) + + } + }, [discoveredDevices, isSuccess, isRefetching, refetch]) return ( !isFetching && !isError ? - + {discoveredDevices - // .filter(device => !connectedDevicesIds.includes(device.address)) - .map((device, i) => )} + .map(device => {delay(2, refetchConnected)} }/>)} : <> ); -} \ No newline at end of file +} + diff --git a/src/components/devices/linked/DeviceActionChips.js b/src/components/devices/linked/DeviceActionChips.js index 5ffd068..10bf35f 100644 --- a/src/components/devices/linked/DeviceActionChips.js +++ b/src/components/devices/linked/DeviceActionChips.js @@ -1,13 +1,15 @@ import { Button, Flex } from "@chakra-ui/react"; import { useExecuteActionMutation } from "api/services/devicesApi"; +import { useState } from "react"; export const DeviceActionChips = ({ device }) => { if(!device.actions || device.actions.length < 1) { return <> } + return ( - + {device.actions.map((action, index) => )} ) @@ -17,18 +19,48 @@ const DeviceActionChip = ({ deviceId, actionName }) => { const [executeAction, { isLoading }] = useExecuteActionMutation(); return ( ); } +export const DeviceSelectableActionChip = ({ actionName , onClick, initialSelected}) => { + const [isSelected, setSelected] = useState(!!initialSelected()); + return ( + {onClick(); setSelected(!isSelected)}} + /> + ); +} + +const DeviceActionChipButton = ({name, onClick, ...props}) => { + return ( + + ); +} diff --git a/src/components/devices/linked/DeviceFullViewModal.js b/src/components/devices/linked/DeviceFullViewModal.js index fc5915c..61f6961 100644 --- a/src/components/devices/linked/DeviceFullViewModal.js +++ b/src/components/devices/linked/DeviceFullViewModal.js @@ -1,39 +1,68 @@ import { ChevronLeftIcon, ChevronRightIcon, DeleteIcon, EditIcon } from "@chakra-ui/icons"; -import { AlertDialog, AlertDialogBody, AlertDialogContent, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, Badge, border, Box, Button, Center, Divider, Flex, FormControl, FormLabel, HStack, IconButton, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, SimpleGrid, Spacer, Stack, Table, TableCaption, TableContainer, Tbody, Td, Text, Textarea, Tfoot, Th, Thead, Tr, useDisclosure, VStack } from "@chakra-ui/react"; +import { AlertDialog, AlertDialogBody, AlertDialogContent, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, Badge, Button, Center, Divider, Flex, FormControl, FormLabel, HStack, IconButton, Input, Link, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, SimpleGrid, Spacer, Stack, Table, TableCaption, TableContainer, Tbody, Td, Text, Textarea, Tfoot, Th, Thead, Tr, useDisclosure, VStack } from "@chakra-ui/react"; import { DEVICE_DESCRIPTION_LENGTH } from "api/contants"; import { useDeleteDeviceMutation, useGetReportsPageQuery, usePatchDeviceMutation } from "api/services/devicesApi"; import { RefreshButton } from "components/button/RefreshButton"; import { DeviceActionChips } from "components/devices/linked/DeviceActionChips"; import { WarningBadge } from "components/info/WarningItem"; +import { SpinnerContainer } from "components/spinner/SpinnerContainer"; import { Field, Form, Formik } from "formik"; import { usePagination } from "hooks/usePagination"; import { useEffect, useRef, useState } from "react"; import { useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom"; import { useFetchDevicesQuery } from "store/slice/devicesSlice"; -import { useBorderColors } from "styles/theme/foundations/colors"; -import { coalesce } from "utils/utils"; +import { useBorderColors, useColors } from "styles/theme/foundations/colors"; +import { coalesce, coalesceOrEmpty } from "utils/utils"; export const DeviceFullViewModal = ({ isOpen, onOpen, onClose: closeModal }) => { + const [isSaving, setSaving] = useState(false); const activeDeviceAddress = useSelector(state => state.devices.activeDeviceAddress); const { data: entities } = useFetchDevicesQuery(); - const [updateDevice, { isLoading, isError, error }] = usePatchDeviceMutation(activeDeviceAddress); - const [deleteDevice, { isLoading: isDeleting }] = useDeleteDeviceMutation(); + const [updateDevice, { isLoading, isError, isSuccess, error }] = usePatchDeviceMutation(activeDeviceAddress); + const [deleteDevice, { isLoading: isDeleting, isError: isDeleteError, isSuccess: isDeleteSuccess }] = useDeleteDeviceMutation(); const [isEditMode, setEditMode] = useState(false); const initialRef = useRef(); + const loading = isLoading || isDeleting; + + useEffect(() => { + if (isSaving && !isLoading && !isError && isSuccess) { + setSaving(false) + closeModal(); + } else if (isSaving && !isDeleting && !isDeleteError && isDeleteSuccess) { + setSaving(false) + closeModal(); + } else if (isSaving && !loading && isError) { + setSaving(false) + } else if (isSaving && !loading && isDeleteError) { + setSaving(false) + } + }, [loading, isSaving]) + if (!isOpen) { return <> + } else if (loading && isSaving) { + return } const device = entities[activeDeviceAddress] - const name = coalesce(device.name, ""); - const description = coalesce(device.description, ""); - const onClose = () => { setEditMode(false); closeModal() } + const name = coalesceOrEmpty(device.name, ""); + const description = coalesceOrEmpty(device.description, ""); + const onClose = () => { + setEditMode(false); + setSaving(true); + } + + const close = () => { + setEditMode(false); + closeModal(); + } const SaveButton = return ( { setEditMode(false); onClose() }} + onClose={close} initialFocusRef={initialRef} size="6xl" > @@ -42,10 +71,8 @@ export const DeviceFullViewModal = ({ isOpen, onOpen, onClose: closeModal }) => initialValues={{ name: name, description: description }} onSubmit={(values, { setSubmitting }) => { updateDevice({ deviceId: device.id, ...values }) - if (!isError) { - setSubmitting(false) - onClose() - } + setSubmitting(false) + onClose(); }} > {({ handleSubmit, errors, touched }) => ( @@ -98,7 +125,7 @@ export const DeviceFullViewModal = ({ isOpen, onOpen, onClose: closeModal }) => {SaveButton} - + @@ -111,6 +138,8 @@ export const DeviceFullViewModal = ({ isOpen, onOpen, onClose: closeModal }) => const DeviceModalAdditionalData = ({ device }) => { + const warningColor = useColors().warning; + let navigate = useNavigate(); return ( Device data: @@ -128,8 +157,9 @@ const DeviceModalAdditionalData = ({ device }) => { - {device.reportTypes && + {device.reportTypes && device.reportTypes.length > 0 && <> + navigate(`/devices/${device.id}/triggers`)}>View device triggers Known report types {device.reportTypes.map(reportType => {reportType})} @@ -210,8 +240,9 @@ const ReportItem = ({ number, report }) => { // type | date created | data return ( - {number}{report.type} - {new Date(report.dateTimeCreated).toLocaleDateString("ru-RU", { year: "numeric", month: "numeric", day: "numeric" })} + {number}{report.reportType} + {new Date(report.dateTimeCreated).toLocaleDateString("ru-RU", + { year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric" })} {{reportData}} ) diff --git a/src/components/info/WarningItem.js b/src/components/info/WarningItem.js index 1c25da9..2091bbf 100644 --- a/src/components/info/WarningItem.js +++ b/src/components/info/WarningItem.js @@ -1,4 +1,5 @@ -import { Tooltip } from "@chakra-ui/react"; +import { WarningIcon } from "@chakra-ui/icons"; +import { Text, Tooltip } from "@chakra-ui/react"; import { useColors } from "styles/theme/foundations/colors"; @@ -6,7 +7,7 @@ export const WarningBadge = ({ message }) => { const warningColor = useColors().warning; return ( - + ) } \ No newline at end of file diff --git a/src/components/link/links.js b/src/components/link/links.js index a7c4772..d8400e5 100644 --- a/src/components/link/links.js +++ b/src/components/link/links.js @@ -6,5 +6,9 @@ export const navLinks = [ { title: "Connect", to: "/connect" + }, + { + title: "Hooks", + to: "/hooks" } ]; diff --git a/src/components/nav/NavBar.js b/src/components/nav/NavBar.js index dc1db94..629f283 100644 --- a/src/components/nav/NavBar.js +++ b/src/components/nav/NavBar.js @@ -1,24 +1,16 @@ import { - Box, Collapse, - Flex, - Stack, - Text, - useColorModeValue, - useDisclosure + Flex, useDisclosure } from "@chakra-ui/react"; -import { LogoLink } from "components/link/LogoLink"; -import { Link } from "components/link/Link"; +import { SettingsButton } from "components/button/SettingsButton"; import { ThemeToggleButton } from "components/button/ThemeToggleButton"; -import { MobileNav, MobileNavBurger } from "./MobileNav"; -import { DesktopNav } from "./DesktopNav"; +import { LogoLink } from "components/link/LogoLink"; import { - useLinkColors, useBackgroundColors, - useBorderColors, - useTextColors + useBorderColors } from "styles/theme/foundations/colors"; -import { appLinks } from "components/link/TextLink"; +import { DesktopNav } from "./DesktopNav"; +import { MobileNav, MobileNavBurger } from "./MobileNav"; export const NavBar = () => { const { isOpen, onToggle } = useDisclosure(); @@ -44,6 +36,7 @@ export const NavBar = () => { + ( - - {leftSide && ( - - - {leftSide} - + <> + + + {leftSide && ( + + + {leftSide} + + + )} + {rightSide && rightSide} - )} - {rightSide && rightSide} - + + + ); diff --git a/src/components/panel/actions/RefreshAction.js b/src/components/panel/actions/RefreshAction.js index 4fa60bf..fab6e21 100644 --- a/src/components/panel/actions/RefreshAction.js +++ b/src/components/panel/actions/RefreshAction.js @@ -1,23 +1,25 @@ -import { Flex, FormControl, FormLabel, HStack, Kbd } from "@chakra-ui/react"; +import { Flex, FormControl, FormLabel, HStack, Kbd, Tooltip } from "@chakra-ui/react"; import { RefreshButton } from "components/button/RefreshButton"; import { isHotkeyPressed, useHotkeys } from "react-hotkeys-hook"; -export const RefreshAction = ({ refreshAction, refreshHotkeys, title, isLoading, ...props }) => { +export const RefreshAction = ({ refreshAction, refreshHotkeys, title, isLoading, isDisabled, tooltip, ...props }) => { useHotkeys(refreshHotkeys.toLowerCase(), () => { if (!isLoading) { refreshAction() } }); return ( - - - Refresh - refreshAction()} - isLoading={isLoading} - /> - - - + + + + refreshAction()} + isLoading={isLoading} + isDisabled={isDisabled} + /> + + + + ); }; diff --git a/src/components/spinner/SpinnerContainer.js b/src/components/spinner/SpinnerContainer.js index ddd13de..bed75d1 100644 --- a/src/components/spinner/SpinnerContainer.js +++ b/src/components/spinner/SpinnerContainer.js @@ -4,21 +4,27 @@ import { Center, Text } from "@chakra-ui/react"; +import { useBackgroundColors, useColors } from "styles/theme/foundations/colors"; -export const SpinnerContainer = ({isLoading, error, ...props}) => ( - - {isLoading && ( -
- -
- )} - {!isLoading && error && ( -
- {"Error: " + error.message} -
- )} - {!isLoading && ( - props.children - )} -
-) +export const SpinnerContainer = ({ isLoading, error, ...props }) => { + const bgColor = useBackgroundColors().card; + return ( + + {isLoading && ( +
+
+ +
+
+ )} + {!isLoading && error && ( +
+ {"Error: " + error.message} +
+ )} + {!isLoading && !error && ( + props.children + )} +
+ ) +} diff --git a/src/components/text/PageTitle.js b/src/components/text/PageTitle.js new file mode 100644 index 0000000..10cfd95 --- /dev/null +++ b/src/components/text/PageTitle.js @@ -0,0 +1,6 @@ +import { Heading, Text } from "@chakra-ui/react" + + +export const PageTitle = (props) => { + return {props.children} +} \ No newline at end of file diff --git a/src/components/widget/DeviceWidget.js b/src/components/widget/DeviceWidget.js index 2cb8a5a..b82caa3 100644 --- a/src/components/widget/DeviceWidget.js +++ b/src/components/widget/DeviceWidget.js @@ -1,6 +1,8 @@ import { - Box, Divider, Flex, IconButton, Spacer, Text, VStack + Badge, + Box, Center, Divider, Flex, IconButton, Spacer, Text, VStack } from "@chakra-ui/react"; +import { DEVICE_STATUS_WAITING_CONFIGURATION } from "api/contants"; import { DeviceActionChips } from "components/devices/linked/DeviceActionChips"; import { ChakraIcon } from "components/icon/ChakraIcon"; import { IoResize } from "react-icons/io5"; @@ -27,8 +29,14 @@ const DeviceWidget = ({ device, onOpen, ...props }) => ( itemAddress={device.address} onOpen={onOpen} /> - - {props.children} + {device.status === DEVICE_STATUS_WAITING_CONFIGURATION + ?
Device is waiting configuration
+ : <> + + + {props.children} + + } ) diff --git a/src/components/widget/DeviceWidgetGrid.js b/src/components/widget/DeviceWidgetGrid.js index 27de5bc..1b852a9 100644 --- a/src/components/widget/DeviceWidgetGrid.js +++ b/src/components/widget/DeviceWidgetGrid.js @@ -1,26 +1,28 @@ import { Button, HStack, SimpleGrid, Stack, Text, useDisclosure } from "@chakra-ui/react"; import { DeviceFullViewModal } from "components/devices/linked/DeviceFullViewModal"; import { Link } from "components/link/Link"; +import { useErrorToast } from "hooks/useErrorToast"; import { useFetchDevicesQuery } from "store/slice/devicesSlice"; import { delay } from "utils/utils"; import { DeviceWidget } from "./DeviceWidget"; const DeviceWidgetGrid = () => { const { isOpen, onOpen, onClose } = useDisclosure(); - const { data: devices, isLoading, refetch } = useFetchDevicesQuery(); + const { data: devices, isLoading, refetch, error } = useFetchDevicesQuery(); + useErrorToast(error) if (devices && Object.keys(devices).length > 0) { return ( <> {mapDevicesToWidgets(devices, onOpen)} - {onClose(); delay(1, refetch)}} /> + {onClose(); refetch()}} /> ); } else if(!isLoading) { return ( - No devices were found. Try connect new one: + No devices were found. Try connect a new one: ); diff --git a/src/hooks/useErrorToast.js b/src/hooks/useErrorToast.js new file mode 100644 index 0000000..71dbff9 --- /dev/null +++ b/src/hooks/useErrorToast.js @@ -0,0 +1,16 @@ +import { useToast } from "@chakra-ui/react" +import { useEffect } from "react" + + +export const useErrorToast = (errorState) => { + const toast = useToast(); + const id = "show-error-toast" + useEffect(() => { + if (errorState && !toast.isActive(id)) { + toast({ + id, + title: "boo" + }) + } + }, [errorState, toast]) +} \ No newline at end of file diff --git a/src/pages/AddDeviceTriggersPage.js b/src/pages/AddDeviceTriggersPage.js new file mode 100644 index 0000000..a8c7cc5 --- /dev/null +++ b/src/pages/AddDeviceTriggersPage.js @@ -0,0 +1,206 @@ +import { Accordion, AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Badge, Box, Button, Center, Divider, Flex, Heading, SimpleGrid, Text, Tooltip, VStack } from "@chakra-ui/react"; +import { useCreateTriggersMutation, useGetAllHooksQuery, useGetDeviceTriggersQuery } from "api/services/hooksApi"; +import { RefreshButton } from "components/button/RefreshButton"; +import { ActionPanel } from "components/panel/ActionPanel"; +import { SpinnerContainer } from "components/spinner/SpinnerContainer"; +import { PageTitle } from "components/text/PageTitle"; +import { useState } from "react"; +import { useParams } from "react-router-dom"; +import { useFetchDevicesQuery } from "store/slice/devicesSlice"; +import { HookCard } from "./hooks/HookCard"; + + + +export const AddDeviceTriggersPage = () => { + const { deviceId } = useParams(); + const { data: devices, refetch, isLoading: isDeviceLoading } = useFetchDevicesQuery(); + const { data: triggers, isLoading: isTriggersLoading, refetch: refetchTriggers } = useGetDeviceTriggersQuery(deviceId); + const { data: hooks, isHooksLoading, refetch: refetchHooks } = useGetAllHooksQuery(); + const refresh = () => { refetch(); refetchTriggers(); refetchHooks() } + const tmp = Object.values(devices).filter(device => device.id === deviceId); + const device = tmp.length > 0 ? tmp[0] : undefined + + if (!device) { + return ( +
404 Error: Device not found by id={deviceId}
+ ) + } + + return ( + + + + Device's triggers} + rightSide={ + refresh()} + isLoading={isDeviceLoading || isTriggersLoading || isHooksLoading} /> + } + /> + + + + + + + + ) +} + +const TriggerCreator = ({ device }) => { + if (!device.reportTypes || device.reportTypes.length === 0) { + return (Device doesn't have report types to create triggers) + } + return ( + <> + + + + + Create new device reports triggers + + + + + + + + + + + ) +} + +const CreateTriggersForm = ({ device }) => { + const [createTriggers] = useCreateTriggersMutation(); + const { data: hooks } = useGetAllHooksQuery(); + const [selectedTypes, setSelectedTypes] = useState({}); + const [selectedHooks, setSelectedHooks] = useState({}); + const [isSaveActive, setSaveActive] = useState(false); + const toggleSelectedHook = (hookId) => { + setSelectedHooks(hooks => { + hooks[hookId] = !hooks[hookId] + toggleSaveActive() + return selectedHooks + }) + } + const toggleSelectedType = (type) => { + setSelectedTypes(types => { + types[type] = !types[type] + toggleSaveActive() + return selectedTypes + }) + } + + const toggleSaveActive = () => { + setSaveActive( + Object.values(selectedTypes).some(obj => !!obj) && + Object.values(selectedHooks).some(obj => !!obj) + ) + } + const onSave = () => { + const hookIds = Object.entries(selectedHooks).filter(([_, selected]) => !!selected).map(([id, _]) => id) + const reportTypes = Object.entries(selectedTypes).filter(([_, selected]) => !!selected).map(([type, _]) => type) + createTriggers({ + deviceId: device.id, + hookIds: hookIds, + reportTypes: reportTypes + }) + console.log({ + deviceId: device.id, + hookIds: hookIds, + reportTypes: reportTypes + }) + setSelectedHooks({}) + setSelectedTypes({}) + setSaveActive(false) + } + const Chip = ({ id, name, initialChecked, callback }) => { + const [isChecked, setChecked] = useState(initialChecked); + return ( + + + + ) + } + return ( + <> + + + + Select device report types, that will trigger hooks + + {device.reportTypes && device.reportTypes.length > 0 + ? device.reportTypes.map(reportType => ( toggleSelectedType(type)} />) + ) + : No device report types available + } + + + + + Select hooks to trigger + + {hooks + ? hooks.map(hook => ( toggleSelectedHook(id)} />) + ) + : No hooks available + } + + + + + + ) +} + +const TriggersAndHooks = ({ triggers, hooks, refresh }) => { + if (!triggers || triggers.length === 0 || !hooks || hooks.length === 0) return <> + const grouped = triggers.reduce((group, trigger) => { + const { reportType } = trigger; + group[reportType] = group[reportType] ?? []; + group[reportType].push(trigger); + return group; + }, {}); + + return ( + + What hooks are called + { + Object.entries(grouped).map(([type, typeTriggers]) => ( + + + When report type is + {type} + + + {typeTriggers.map(trigger => ( + hook.id === trigger.hookId)[0]} /> + ))} + + + + )) + } + + ) +} \ No newline at end of file diff --git a/src/pages/ConnectPage.js b/src/pages/ConnectPage.js index 293c99e..de4dad0 100644 --- a/src/pages/ConnectPage.js +++ b/src/pages/ConnectPage.js @@ -1,23 +1,42 @@ -import { WarningIcon } from "@chakra-ui/icons"; -import { FormControl, FormLabel, HStack, Stack, Switch, Tooltip, useColorModeValue } from "@chakra-ui/react"; +import { FormControl, FormLabel, HStack, Link, Stack, Switch, Text, Tooltip, useToast } from "@chakra-ui/react"; +import { APP_SETTINGS_TYPE, useGetSettingsStatusQuery } from "api/services/appSettingsApi"; import { useGetDiscoveredDevicesQuery } from "api/services/discoveryApi"; import { DiscoveredDevices } from "components/devices/discovery/DiscoveredDevices"; -import { WarningBadge } from "components/info/WarningItem"; import { ActionPanel } from "components/panel/ActionPanel"; import { RefreshAction } from "components/panel/actions/RefreshAction"; import { SpinnerContainer } from "components/spinner/SpinnerContainer"; import { useCooldown } from "hooks/useCooldown"; import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; import { useFetchDevicesQuery } from "store/slice/devicesSlice"; import { useDiscoveryStatus } from "store/slice/discoverySlice"; -import { delay } from "utils/utils"; export const ConnectPage = () => { + const { data: settingsStatuses, isLoading: isGetStatusLoading, isSuccess: isGetStatusSuccess } = useGetSettingsStatusQuery(); + const wifiSettingsConfigured = settingsStatuses && !!settingsStatuses[APP_SETTINGS_TYPE]; + const configureWifiTip = !isGetStatusLoading && wifiSettingsConfigured ? "" : "Please, configure Wi-Fi settings"; + const toast = useToast(); + let navigate = useNavigate(); + useEffect(() => { + if (!toast.isActive("wifi-config-toast") && isGetStatusSuccess && !isGetStatusLoading && !wifiSettingsConfigured) + toast({ + position: "top", + id: "wifi-config-toast", + title: "Wi-FI configuration missing", + description: { toast.close("wifi-config-toast"); navigate("/settings") }}>{configureWifiTip}, + duration: 25000, + isClosable: true, + status: "warning", + }) + }, + [wifiSettingsConfigured, isGetStatusLoading, configureWifiTip, navigate, toast] + ) + const { data: isDiscoveryActive, refetch: refetchStatus, isError: isStatusError } = useDiscoveryStatus(); const { data: discoveredDevices, isLoading, refetch: refetchDiscoveredDevices, isError: isDevicesError } = useGetDiscoveredDevicesQuery(); - const {refetch: refetchDevices} = useFetchDevicesQuery(); + const { refetch: refetchDevices } = useFetchDevicesQuery(); const isError = isStatusError || isDevicesError; - const refetchAllDevices = () => {refetchDevices(); refetchDiscoveredDevices()} + const refetchAllDevices = () => { refetchDevices(); refetchDiscoveredDevices() } useEffect(refetchAllDevices, [isDiscoveryActive]); return ( @@ -26,14 +45,18 @@ export const ConnectPage = () => { Activate discovery - - {isError && } + } rightSide={ - { refetchStatus(); refetchAllDevices(); }} refreshHotkeys="Alt+R" title="Refresh devices" /> + { refetchStatus(); refetchAllDevices(); }} + refreshHotkeys="Alt+R" title="Refresh devices" + /> } /> @@ -47,17 +70,41 @@ export const ConnectPage = () => { ); }; -const DiscoverySwitch = () => { +const DiscoverySwitch = ({ isDisabled, tooltip }) => { + + const toast = useToast(); const [isCooledDown, startCooldown] = useCooldown(500); - const { data: discoveryStatus, isLoading, updateDiscoveryStatus, isError: isStatusError } = useDiscoveryStatus(); + const { + data: discoveryStatus, + isLoading, + updateDiscoveryStatus + } = useDiscoveryStatus(() => showErrorToast(toast)); + + return ( + + { /* To prevent tooltip bug */} + { startCooldown(); updateDiscoveryStatus(!discoveryStatus) }} + /> + + + ) +} - return - { startCooldown(); updateDiscoveryStatus(!discoveryStatus) }} - /> - +const showErrorToast = (toast) => { + const toastId = "showDiscoveryUpdateErrorToast" + if (!toast.isActive(toastId)) { + toast({ + id: toastId, + title: "An error occurred", + description: "Error while trying to change discovery status", + status: "error", + duration: 4000, + isClosable: true + }) + } } \ No newline at end of file diff --git a/src/pages/Home.js b/src/pages/Home.js index a1e037e..d8b3b28 100644 --- a/src/pages/Home.js +++ b/src/pages/Home.js @@ -1,5 +1,7 @@ -import { Stack, Text } from "@chakra-ui/react"; +import { Flex } from "@chakra-ui/react"; import { ActionPanel } from "components/panel/ActionPanel"; +import { SpinnerContainer } from "components/spinner/SpinnerContainer"; +import { PageTitle } from "components/text/PageTitle"; import { DeviceWidgetGrid } from "components/widget/DeviceWidgetGrid"; import { DeviceWidgetGridActions } from "components/widget/DeviceWidgetGridActions"; import { useFetchDevicesQuery } from "store/slice/devicesSlice"; @@ -7,13 +9,13 @@ import { useFetchDevicesQuery } from "store/slice/devicesSlice"; const Home = () => { const { isLoading, refetch } = useFetchDevicesQuery(); return ( - + Linked devices} + leftSide={Linked devices} rightSide={} /> - - + +
); } diff --git a/src/pages/Main.js b/src/pages/Main.js index 9acb11f..9e6f4b8 100644 --- a/src/pages/Main.js +++ b/src/pages/Main.js @@ -12,10 +12,8 @@ export const Main = () => { color={useTextColors().default} maxW={"7xl"} px={4} - py={3} display="flex" flexGrow={1} - my="0.5rem" > diff --git a/src/pages/hooks/CreateHookModal.js b/src/pages/hooks/CreateHookModal.js new file mode 100644 index 0000000..c43cb8c --- /dev/null +++ b/src/pages/hooks/CreateHookModal.js @@ -0,0 +1,49 @@ +import { Button, Flex, Heading, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Select } from "@chakra-ui/react"; +import { useRef, useState } from "react"; +import { EmailHookCreator } from "./HookCreators"; + + + +export const CreateHookModal = ({ isOpen, onOpen, onClose: closeModal }) => { + const [hookType, setHookType] = useState(undefined); + const initialRef = useRef(); + if (!isOpen) { + return <> + } + + const onClose = () => { + closeModal(); + } + return ( + <> + + + + + Create new hook: + + + + + + { + hookType && + hookType === "sendEmail" ? + : <> + } + + + + + + + + ); +} diff --git a/src/pages/hooks/HookCard.js b/src/pages/hooks/HookCard.js new file mode 100644 index 0000000..a5b636f --- /dev/null +++ b/src/pages/hooks/HookCard.js @@ -0,0 +1,29 @@ +import { Divider, Flex, Text, VStack } from "@chakra-ui/react" +import { useDeleteHookMutation } from "api/services/hooksApi" +import { DeleteButton } from "components/button/DeleteButton" +import { useBorderColors } from "styles/theme/foundations/colors" +import { coalesce, delay } from "utils/utils" + +export const HookCard = ({ hook, refresh }) => { + const [deleteHook] = useDeleteHookMutation() + return ( + } + > + + Name: {coalesce(hook.name, "[None]")} + { deleteHook(hook.id); delay(1, refresh()) }} /> + + Type: {coalesce(hook.type, "[None]")} + Id: {coalesce(hook.id, "[None]")} + Description: {coalesce(hook.description, "[None]")} + + ) +} \ No newline at end of file diff --git a/src/pages/hooks/HookCreators.js b/src/pages/hooks/HookCreators.js new file mode 100644 index 0000000..4b405e1 --- /dev/null +++ b/src/pages/hooks/HookCreators.js @@ -0,0 +1,66 @@ +import { HOOK_TYPE_SEND_EMAIL } from "api/contants"; +import { useCreateEmailHookMutation } from "api/services/hooksApi"; + +const { HStack, Stack, Input, Heading, FormLabel, Button, Center } = require("@chakra-ui/react"); +const { Formik, Form, Field } = require("formik") + + + +export const EmailHookCreator = ({ onClose }) => { + const [createHook, { isLoading }] = useCreateEmailHookMutation(); + return ( + { + createHook({ + name: values.name, + description: values.description, + emailAddress: { address: values.email }, + type: HOOK_TYPE_SEND_EMAIL + }) + setSubmitting(false) + onClose(); + }} + > + {({ handleSubmit, errors, touched }) => ( +
+ + Send email hook + Hook name + + Hook description + + Email address for letters with reports + + + +
+
+ )} +
+ ) +} \ No newline at end of file diff --git a/src/pages/hooks/HooksPage.js b/src/pages/hooks/HooksPage.js new file mode 100644 index 0000000..9279c6d --- /dev/null +++ b/src/pages/hooks/HooksPage.js @@ -0,0 +1,42 @@ +import { Flex, HStack, SimpleGrid, useDisclosure } from "@chakra-ui/react" +import { useGetAllHooksQuery } from "api/services/hooksApi" +import { PlusButton } from "components/button/PlusButton" +import { RefreshButton } from "components/button/RefreshButton" +import { ActionPanel } from "components/panel/ActionPanel" +import { SpinnerContainer } from "components/spinner/SpinnerContainer" +import { PageTitle } from "components/text/PageTitle" +import { delay } from "utils/utils" +import { CreateHookModal } from "./CreateHookModal" +import { HookCard } from "./HookCard" + + + +export const HooksPage = () => { + const { data: hooks, isLoading, refetch } = useGetAllHooksQuery(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const refresh = () => { + console.log("refresh") + delay(1, () => refetch()); + } + return ( + <> + + Hooks} + rightSide={ + onOpen()} /> + + } + /> + + + + {hooks && hooks.map(hook => )} + + + + + { refresh(); onClose(); }} /> + + ) +} \ No newline at end of file diff --git a/src/pages/settings/SettingsPage.js b/src/pages/settings/SettingsPage.js new file mode 100644 index 0000000..9bf0148 --- /dev/null +++ b/src/pages/settings/SettingsPage.js @@ -0,0 +1,70 @@ +import { Box, Divider, Flex, HStack, Link, Text, VStack } from "@chakra-ui/react" +import { ActionPanel } from "components/panel/ActionPanel" +import { PageTitle } from "components/text/PageTitle" +import React from "react" +import { useBackgroundColors, useBorderColors } from "styles/theme/foundations/colors" +import { MailSettingsSection } from "./sections/MailSettingsSection" +import { WifiSettingsSection } from "./sections/WifiSettingsSection" + + +export const SettingsPage = () => { + const hoverColor = useBackgroundColors().actionHover; + return ( + + Settings} /> + + + } + > + {Object.keys(settingsBlocks).map((sectionName) => + + + + {sectionName} + + + + ) + } + + + }> + { + Object.entries(settingsBlocks).map(([name, sectionCreator]) => ( + + {sectionCreator(name)} + + )) + } + + + + + ) +} + +const settingsBlocks = { + "Wi-Fi settings": (sectionName) => , + "Mail settings": (sectionName) => +} + +export const getAnchorFromString = (str) => str + ? str.replace(" ", "_").toLowerCase() + : "" + + + //} > +//Object.keys(settingsBlocks).map((sectionName) \ No newline at end of file diff --git a/src/pages/settings/sections/MailSettingsSection.js b/src/pages/settings/sections/MailSettingsSection.js new file mode 100644 index 0000000..0ebd2f4 --- /dev/null +++ b/src/pages/settings/sections/MailSettingsSection.js @@ -0,0 +1,52 @@ +import { Input } from "@chakra-ui/react" +import { EMAIL_MAX_LENGTH, EMAIL_MIN_LENGTH, EMAIL_PASSWORD_MAX_LENGTH, EMAIL_PASSWORD_MIN_LENGTH } from "api/contants" +import { MAIL_SETTINGS_TYPE, useGetSettingsByTypeQuery, useUpdateMailSettingsMutation } from "api/services/appSettingsApi" +import { SpinnerContainer } from "components/spinner/SpinnerContainer" +import { Field } from "formik" +import { SettingsSection } from "./SettingsSection" + + +export const MailSettingsSection = ({sectionName}) => { + const {data: mailSettings, isLoading} = useGetSettingsByTypeQuery({type: MAIL_SETTINGS_TYPE}); + const [updateMailSettings ,{}] = useUpdateMailSettingsMutation(); + const sectionInputs = { + "Email username": + , + "Email password": + + } + const initialValues = { + email: mailSettings?.mailUsername, + password: mailSettings?.mailPassword + } + return ( + + updateMailSettings({ + mailUsername: values.email.split(""), + mailPassword: values.password.split(""), + type: MAIL_SETTINGS_TYPE + })} + initialValues={initialValues} + labelsToFields={sectionInputs} + /> + + ) +} \ No newline at end of file diff --git a/src/pages/settings/sections/SettingsSection.js b/src/pages/settings/sections/SettingsSection.js new file mode 100644 index 0000000..5b8485a --- /dev/null +++ b/src/pages/settings/sections/SettingsSection.js @@ -0,0 +1,36 @@ +import { Box, Button, Flex, FormLabel, Grid, GridItem, Heading, Link } from "@chakra-ui/react"; +import { Form, Formik } from "formik"; +import React from "react"; +import { getAnchorFromString } from "../SettingsPage"; + + +export const SettingsSection = ({ name, initialValues, onSubmit, labelsToFields }) => { + const anchor = getAnchorFromString(name); + return ( + + + {name} + # + + + {({ handleSubmit, values, errors }) => ( +
+ + { + Object.entries(labelsToFields).map(([label, field], i) => ( + + {createLabel(field.props.id, label)} + {field} + + )) + } + + +
+ ) + } +
+
) +} + +export const createLabel = (forId, value) => {value} \ No newline at end of file diff --git a/src/pages/settings/sections/WifiSettingsSection.js b/src/pages/settings/sections/WifiSettingsSection.js new file mode 100644 index 0000000..0b3a4ec --- /dev/null +++ b/src/pages/settings/sections/WifiSettingsSection.js @@ -0,0 +1,51 @@ +import { Input } from "@chakra-ui/react" +import { WIFI_PASSWORD_MIN_LENGTH, WIFI_SSID_MIN_LENGTH } from "api/contants" +import { APP_SETTINGS_TYPE, useGetAppSettingsQuery, useGetSettingsByTypeQuery, useUpdateAppSettingsMutation } from "api/services/appSettingsApi" +import { Field } from "formik" +import { useEffect } from "react" +import { coalesce, coalesceOrEmpty } from "utils/utils" +import { SettingsSection } from "./SettingsSection" + + +export const WifiSettingsSection = ({ sectionName }) => { + const { data: wifiSettings, isLoading } = useGetAppSettingsQuery(); + const [updateAppSettings, { isError: isUpdateError }] = useUpdateAppSettingsMutation(); + const sectionInputs = { + "Wi-Fi network name": + , + "Wi-Fi password": + + } + const initialValues = { + ssid: wifiSettings?.ssid, + password: wifiSettings?.password + } + return ( + isLoading + ? <> + : updateAppSettings({ + ssid: values.ssid.split(""), + password: values.password.split(""), + type: APP_SETTINGS_TYPE + })} + initialValues={initialValues} + labelsToFields={sectionInputs} + /> + ) +} \ No newline at end of file diff --git a/src/store/slice/discoverySlice.js b/src/store/slice/discoverySlice.js index 6101fb3..806b888 100644 --- a/src/store/slice/discoverySlice.js +++ b/src/store/slice/discoverySlice.js @@ -23,15 +23,18 @@ const getDiscoveryStatus = createAsyncThunk( const putDiscoveryStatus = createAsyncThunk( "discovery/status/put", - async (activateDiscovery, { getState, requestId, dispatch, rejectWithValue }) => { + async ({isActivateDiscovery, doOnError}, { getState, requestId, dispatch, rejectWithValue }) => { const { currentRequestId, loadingStatus } = getState().discovery if (loadingStatus !== PENDING || currentRequestId !== requestId) { return } - const url = buildApiUrl("/discovery/status", { setActive: activateDiscovery }); + const url = buildApiUrl("/discovery/status", { setActive: isActivateDiscovery }); const discoveryStatus = await axios.post(url) - .catch(error => rejectWithValue(error.message)); + .catch(error => { + rejectWithValue(error.message); + doOnError(); + }); dispatch(setDiscoveryStatus(discoveryStatus.data.isActive)) } ) @@ -104,10 +107,11 @@ export const discoverySlice = createSlice({ } }); -export const useDiscoveryStatus = () => { +export const useDiscoveryStatus = (onErrorCallback) => { const { discoveryStatus, loadingStatus, isError, error } = useSelector(state => state.discovery); const dispatch = useDispatch(); useEffect(() => dispatch(getDiscoveryStatus()), []); + return { data: discoveryStatus, isLoading: loadingStatus === PENDING, @@ -115,7 +119,10 @@ export const useDiscoveryStatus = () => { isError: isError, error: error, refetch: () => dispatch(getDiscoveryStatus()), - updateDiscoveryStatus: (value) => dispatch(putDiscoveryStatus(value)) + updateDiscoveryStatus: (value) => dispatch(putDiscoveryStatus({ + isActivateDiscovery: value, + doOnError: onErrorCallback + })) } } diff --git a/src/store/store.js b/src/store/store.js index 7295780..a49fceb 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -4,17 +4,21 @@ import { devicesReducer } from "./slice/devicesSlice"; import { discoveryApi } from "api/services/discoveryApi"; import { discoveryReducer } from "./slice/discoverySlice"; import { devicesApi } from "api/services/devicesApi"; +import { appSettingsApi } from "api/services/appSettingsApi"; +import { hooksApi } from "api/services/hooksApi"; const rootReducer = combineReducers({ widgets: widgetsReducer, devices: devicesReducer, discovery: discoveryReducer, [devicesApi.reducerPath]: devicesApi.reducer, - [discoveryApi.reducerPath]: discoveryApi.reducer + [discoveryApi.reducerPath]: discoveryApi.reducer, + [appSettingsApi.reducerPath]: appSettingsApi.reducer, + [hooksApi.reducerPath]: hooksApi.reducer }); export const store = configureStore({ reducer: rootReducer, middleware: getDefaultMiddleware => - getDefaultMiddleware().concat(devicesApi.middleware, discoveryApi.middleware) + getDefaultMiddleware().concat(devicesApi.middleware, discoveryApi.middleware, appSettingsApi.middleware, hooksApi.middleware) }); diff --git a/src/styles/theme/foundations/colors.js b/src/styles/theme/foundations/colors.js index fc82370..76655fe 100644 --- a/src/styles/theme/foundations/colors.js +++ b/src/styles/theme/foundations/colors.js @@ -62,9 +62,11 @@ const useBackgroundColors = () => { const navBg = useColorModeValue("white", "gray.800"); const subNavBg = useColorModeValue("gray.50", "gray.900"); const mainBg = useColorModeValue("gray.100", "gray.800"); + const cardBg = useColorModeValue("gray.100", "gray.900"); const footerBg = useColorModeValue("gray.600", "gray.900"); const cardFooterBg = useColorModeValue("gray.200", "gray.600"); const goodBg = useColorModeValue("green.200", "green.200"); + const actionHover = useColorModeValue("gray.100" , "gray.700"); return { default: mainBg, @@ -73,12 +75,14 @@ const useBackgroundColors = () => { good: goodBg, subNav: subNavBg, footer: footerBg, + actionHover: actionHover, + card: cardBg }; }; const useBorderColors = () => { const navBorder = useColorModeValue("gray.200", "gray.600"); - const mainBorder = useColorModeValue("gray.200", "gray.900"); + const mainBorder = useColorModeValue("gray.200", "gray.700"); const footerBorder = useColorModeValue("gray.200", "gray.900"); const widgetBorder = useColorModeValue("gray.300", "gray.600"); const lightBorder = useColorModeValue("gray.300", "gray.500") diff --git a/src/utils/utils.js b/src/utils/utils.js index 53b58e4..1776101 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -1,5 +1,6 @@ export const coalesce = (...args) => args.find((_) => ![null, undefined, ""].includes(_)); +export const coalesceOrEmpty = (...args) => args.find((_) => ![null, undefined].includes(_)); export const delay = (seconds, callback) => setTimeout(callback, seconds * 1000) \ No newline at end of file