From a15eb0db4c6d872fbbe2cdebf08fe961a35795f1 Mon Sep 17 00:00:00 2001 From: Alex Christoffer Rasmussen Date: Sat, 15 Jun 2024 12:26:41 +0200 Subject: [PATCH] Client remote control (#930) --------- Co-authored-by: kellhogs Co-authored-by: Carlos Valente Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> --- apps/client/src/App.tsx | 9 +- apps/client/src/AppRouter.tsx | 4 + apps/client/src/common/api/constants.ts | 1 + .../client-modal/ClientModal.module.scss | 6 + .../client-modal/RedirectClientModal.tsx | 69 ++++++++ .../client-modal/RenameClientModal.tsx | 68 ++++++++ .../IdentifyOverlay.module.scss | 34 ++++ .../identify-overlay/IdentifyOverlay.tsx | 64 ++++++++ .../navigation-menu/NavigationMenu.tsx | 10 +- .../rename-client-modal/RenameClientModal.tsx | 71 -------- apps/client/src/common/hooks/useClientPath.ts | 34 ++++ apps/client/src/common/hooks/useSocket.ts | 6 + apps/client/src/common/stores/clientStore.ts | 82 ++++++++++ .../src/common/stores/connectionName.ts | 26 --- apps/client/src/common/utils/socket.ts | 61 ++++++- .../src/features/app-settings/AppSettings.tsx | 6 +- .../app-settings/panel/Panel.module.scss | 2 +- .../ClientControlPanel.module.scss | 24 +++ .../ClientControlPanel.tsx | 15 ++ .../panel/client-control-panel/ClientList.tsx | 147 +++++++++++++++++ .../app-settings/panel/log-panel/LogPanel.tsx | 17 -- .../NetworkInterfaces.module.scss | 0 .../NetworkInterfaces.tsx | 0 .../NetworkLogExport.module.scss} | 0 .../NetworkLogExport.tsx} | 2 +- .../panel/network-panel/NetworkLogPanel.tsx | 28 ++++ .../features/app-settings/settingsStore.ts | 16 +- apps/client/src/theme/_ontimeStyles.scss | 1 - apps/server/src/adapters/WebsocketAdapter.ts | 151 +++++++++++++----- .../api-integration/integration.controller.ts | 28 ++++ e2e/tests/features/302-client-remote.spec.ts | 44 +++++ .../types/src/definitions/Clients.type.ts | 10 ++ packages/types/src/index.ts | 1 + 33 files changed, 868 insertions(+), 169 deletions(-) create mode 100644 apps/client/src/common/components/client-modal/ClientModal.module.scss create mode 100644 apps/client/src/common/components/client-modal/RedirectClientModal.tsx create mode 100644 apps/client/src/common/components/client-modal/RenameClientModal.tsx create mode 100644 apps/client/src/common/components/identify-overlay/IdentifyOverlay.module.scss create mode 100644 apps/client/src/common/components/identify-overlay/IdentifyOverlay.tsx delete mode 100644 apps/client/src/common/components/navigation-menu/rename-client-modal/RenameClientModal.tsx create mode 100644 apps/client/src/common/hooks/useClientPath.ts create mode 100644 apps/client/src/common/stores/clientStore.ts delete mode 100644 apps/client/src/common/stores/connectionName.ts create mode 100644 apps/client/src/features/app-settings/panel/client-control-panel/ClientControlPanel.module.scss create mode 100644 apps/client/src/features/app-settings/panel/client-control-panel/ClientControlPanel.tsx create mode 100644 apps/client/src/features/app-settings/panel/client-control-panel/ClientList.tsx delete mode 100644 apps/client/src/features/app-settings/panel/log-panel/LogPanel.tsx rename apps/client/src/features/app-settings/panel/{log-panel => network-panel}/NetworkInterfaces.module.scss (100%) rename apps/client/src/features/app-settings/panel/{log-panel => network-panel}/NetworkInterfaces.tsx (100%) rename apps/client/src/features/app-settings/panel/{log-panel/LogExport.module.scss => network-panel/NetworkLogExport.module.scss} (100%) rename apps/client/src/features/app-settings/panel/{log-panel/LogExport.tsx => network-panel/NetworkLogExport.tsx} (94%) create mode 100644 apps/client/src/features/app-settings/panel/network-panel/NetworkLogPanel.tsx create mode 100644 e2e/tests/features/302-client-remote.spec.ts create mode 100644 packages/types/src/definitions/Clients.type.ts diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index e929b66a5..6ebce9acb 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -4,16 +4,15 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import ErrorBoundary from './common/components/error-boundary/ErrorBoundary'; +import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverlay'; import { AppContextProvider } from './common/context/AppContext'; import { ontimeQueryClient } from './common/queryClient'; -import { socketClientName } from './common/stores/connectionName'; import { connectSocket } from './common/utils/socket'; import theme from './theme/theme'; import { TranslationProvider } from './translation/TranslationProvider'; import AppRouter from './AppRouter'; -const preferredClientName = socketClientName.getState().name; -connectSocket(preferredClientName); +connectSocket(); function App() { return ( @@ -24,11 +23,15 @@ function App() {
+
+ +
+ diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx index 2f34d2acf..ba146279a 100644 --- a/apps/client/src/AppRouter.tsx +++ b/apps/client/src/AppRouter.tsx @@ -1,6 +1,7 @@ import { lazy, Suspense } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; +import { useClientPath } from './common/hooks/useClientPath'; import Log from './features/log/Log'; import withPreset from './features/PresetWrapper'; import withData from './features/viewers/ViewWrapper'; @@ -34,6 +35,9 @@ const TimerControl = lazy(() => import('./features/control/playback/TimerControl const MessageControl = lazy(() => import('./features/control/message/MessageControlExport')); export default function AppRouter() { + // handle client path changes + useClientPath(); + return ( diff --git a/apps/client/src/common/api/constants.ts b/apps/client/src/common/api/constants.ts index ea06255b5..936317936 100644 --- a/apps/client/src/common/api/constants.ts +++ b/apps/client/src/common/api/constants.ts @@ -11,6 +11,7 @@ export const RUNTIME = ['runtimeStore']; export const SHEET_STATE = ['sheetState']; export const URL_PRESETS = ['urlpresets']; export const VIEW_SETTINGS = ['viewSettings']; +export const CLIENT_LIST = ['clientList']; // resolve location const location = window.location; diff --git a/apps/client/src/common/components/client-modal/ClientModal.module.scss b/apps/client/src/common/components/client-modal/ClientModal.module.scss new file mode 100644 index 000000000..2c291157e --- /dev/null +++ b/apps/client/src/common/components/client-modal/ClientModal.module.scss @@ -0,0 +1,6 @@ +.buttonSection { + display: grid; + grid-template-columns: 1fr 1fr; + gap: $element-spacing; + margin-top: -$element-spacing; +} diff --git a/apps/client/src/common/components/client-modal/RedirectClientModal.tsx b/apps/client/src/common/components/client-modal/RedirectClientModal.tsx new file mode 100644 index 000000000..163e481ba --- /dev/null +++ b/apps/client/src/common/components/client-modal/RedirectClientModal.tsx @@ -0,0 +1,69 @@ +import { useState } from 'react'; +import { + Button, + Input, + InputGroup, + InputLeftAddon, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, +} from '@chakra-ui/react'; + +import { setClientRemote } from '../../hooks/useSocket'; + +import style from './ClientModal.module.scss'; + +interface RedirectClientModalProps { + id: string; + name?: string; + path?: string; + isOpen: boolean; + onClose: () => void; +} + +export function RedirectClientModal(props: RedirectClientModalProps) { + const { id, isOpen, name = '', path: currentPath = '', onClose } = props; + const [path, setPath] = useState(currentPath); + + const { setRedirect } = setClientRemote; + + const handleRedirect = () => { + if (path !== currentPath && path !== '') { + setRedirect({ target: id, redirect: path }); + } + onClose(); + }; + + const host = `${window.location.origin}/`; + const canSubmit = path !== currentPath && path !== ''; + + return ( + + + + Redirect: {name} + + + + {host} + setPath(event.target.value)} /> + + + +
+ + +
+
+
+
+ ); +} diff --git a/apps/client/src/common/components/client-modal/RenameClientModal.tsx b/apps/client/src/common/components/client-modal/RenameClientModal.tsx new file mode 100644 index 000000000..a66991278 --- /dev/null +++ b/apps/client/src/common/components/client-modal/RenameClientModal.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; +import { + Button, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, +} from '@chakra-ui/react'; + +import { setClientRemote } from '../../hooks/useSocket'; + +import style from './ClientModal.module.scss'; + +interface RenameClientModalProps { + id: string; + name?: string; + isOpen: boolean; + onClose: () => void; +} + +export function RenameClientModal(props: RenameClientModalProps) { + const { id, name: currentName = '', isOpen, onClose } = props; + const [name, setName] = useState(currentName); + + const { setClientName } = setClientRemote; + + const handleRename = () => { + if (name !== currentName && name !== '') { + setClientName({ target: id, rename: name }); + } + onClose(); + }; + + const canSubmit = name !== currentName && name !== ''; + + return ( + + + + Rename: {currentName} + + + setName(event.target.value)} + /> + + +
+ + +
+
+
+
+ ); +} diff --git a/apps/client/src/common/components/identify-overlay/IdentifyOverlay.module.scss b/apps/client/src/common/components/identify-overlay/IdentifyOverlay.module.scss new file mode 100644 index 000000000..ac65bc290 --- /dev/null +++ b/apps/client/src/common/components/identify-overlay/IdentifyOverlay.module.scss @@ -0,0 +1,34 @@ +.overlay { + position: absolute; + top: 0; + left: 0; + margin: 0; + width: 100vw; + height: 100vh; + padding: 0; + box-sizing: border-box; + + border: 10px solid $ui-white; + background-color: $ui-black; + + color: $ui-white; + display: grid; + place-content: center; + text-align: center; + z-index: 100; + cursor: pointer; +} + +.name { + font-weight: 600; + margin-top: -10vh; + line-height: 1em; + font-size: 12vw; + text-transform: uppercase; +} + +.message { + color: $label-gray; + line-height: 2em; + font-size: 3vw; +} diff --git a/apps/client/src/common/components/identify-overlay/IdentifyOverlay.tsx b/apps/client/src/common/components/identify-overlay/IdentifyOverlay.tsx new file mode 100644 index 000000000..2f1d32c36 --- /dev/null +++ b/apps/client/src/common/components/identify-overlay/IdentifyOverlay.tsx @@ -0,0 +1,64 @@ +import { useCallback, useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import { MILLIS_PER_MINUTE } from 'ontime-utils'; + +import { setClientRemote } from '../../hooks/useSocket'; +import { useClientStore } from '../../stores/clientStore'; + +import style from './IdentifyOverlay.module.scss'; + +export default function IdentifyOverlay() { + const clients = useClientStore((store) => store.clients); + const id = useClientStore((store) => store.id); + const showOverlay = clients[id]?.identify; + + if (!showOverlay) { + return null; + } + + const portalRoot = document.getElementById('identify-portal'); + + if (!portalRoot) { + return null; + } + return createPortal(, portalRoot); +} + +function Overlay() { + const clients = useClientStore((store) => store.clients); + const id = useClientStore((store) => store.id); + const name = useClientStore((store) => store.name); + + const timerRef = useRef(null); + + const { setIdentify } = setClientRemote; + const showOverlay = clients[id]?.identify; + + const handleClose = useCallback(() => { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + + setIdentify({ target: id, identify: false }); + }, [id, setIdentify]); + + // start a timer that will close the overlay after some time + useEffect(() => { + if (showOverlay) { + timerRef.current = setTimeout(handleClose, MILLIS_PER_MINUTE); + } + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + }, [showOverlay, id, setIdentify, handleClose]); + + console.log('here2'); + return ( +
+
{name}
+
Click to close
+
+ ); +} diff --git a/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx b/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx index 2e2a4ee19..59ba42937 100644 --- a/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx +++ b/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx @@ -19,10 +19,10 @@ import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical'; import { navigatorConstants } from '../../../viewerConfig'; import useClickOutside from '../../hooks/useClickOutside'; +import { useClientStore } from '../../stores/clientStore'; import { useViewOptionsStore } from '../../stores/viewOptions'; import { isKeyEnter } from '../../utils/keyEvent'; - -import RenameClientModal from './rename-client-modal/RenameClientModal'; +import { RenameClientModal } from '../client-modal/RenameClientModal'; import style from './NavigationMenu.module.scss'; @@ -34,8 +34,10 @@ interface NavigationMenuProps { function NavigationMenu(props: NavigationMenuProps) { const { isOpen, onClose } = props; - const { isOpen: isRenameOpen, onOpen: onRenameOpen, onClose: onRenameClose } = useDisclosure(); + const id = useClientStore((store) => store.id); + const name = useClientStore((store) => store.name); + const { isOpen: isOpenRename, onOpen: onRenameOpen, onClose: onCloseRename } = useDisclosure(); const { fullscreen, toggle } = useFullscreen(); const { toggleMirror } = useViewOptionsStore(); @@ -45,7 +47,7 @@ function NavigationMenu(props: NavigationMenuProps) { return createPortal(