feat: allow renaming connection (#457)

* feat: allow renaming connection
This commit is contained in:
Carlos Valente
2023-07-21 20:15:54 +02:00
committed by GitHub
parent 6ef6b0fdcd
commit db1155ce1a
8 changed files with 180 additions and 21 deletions
+3 -1
View File
@@ -9,6 +9,7 @@ import { AppContextProvider } from './common/context/AppContext';
import { ContextMenuProvider } from './common/context/ContextMenuContext';
import useElectronEvent from './common/hooks/useElectronEvent';
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';
@@ -18,7 +19,8 @@ import AppRouter from './AppRouter';
// @ts-expect-error no types from font import
import('typeface-open-sans');
connectSocket();
const preferredClientName = socketClientName.getState().name;
connectSocket(preferredClientName);
function App() {
const { isElectron, sendToElectron } = useElectronEvent();
@@ -1,6 +1,7 @@
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
import { KeyboardEvent, memo, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Link, useLocation, useSearchParams } from 'react-router-dom';
import { useDisclosure } from '@chakra-ui/react';
import { IoApps } from '@react-icons/all-files/io5/IoApps';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import { IoContract } from '@react-icons/all-files/io5/IoContract';
@@ -14,9 +15,11 @@ import useFullscreen from '../../hooks/useFullscreen';
import { useKeyDown } from '../../hooks/useKeyDown';
import { useViewOptionsStore } from '../../stores/viewOptions';
import RenameClientModal from './rename-client-modal/RenameClientModal';
import style from './NavigationMenu.module.scss';
export default function NavigationMenu() {
function NavigationMenu() {
const location = useLocation();
const { isFullScreen, toggleFullScreen } = useFullscreen();
@@ -27,8 +30,10 @@ export default function NavigationMenu() {
const menuRef = useRef<HTMLDivElement | null>(null);
useClickOutside(menuRef, () => setShowMenu(false));
const { isOpen, onOpen, onClose } = useDisclosure();
const toggleMenu = () => setShowMenu((prev) => !prev);
useKeyDown(toggleMenu, ' ', { isDisabled: searchParams.get('edit') === 'true' });
useKeyDown(toggleMenu, ' ', { isDisabled: searchParams.get('edit') === 'true' || isOpen });
useEffect(() => {
let fadeOut: NodeJS.Timeout | null = null;
@@ -51,6 +56,7 @@ export default function NavigationMenu() {
const isKeyEnter = (event: KeyboardEvent<HTMLDivElement>) => event.key === 'Enter';
const handleFullscreen = () => toggleFullScreen();
const handleMirror = () => toggleMirror();
const showEditFormDrawer = () => {
searchParams.append('edit', 'true');
setSearchParams(searchParams);
@@ -58,6 +64,7 @@ export default function NavigationMenu() {
return createPortal(
<div id='navigation-menu-portal' ref={menuRef} className={mirror ? style.mirror : ''}>
<RenameClientModal isOpen={isOpen} onClose={onClose} />
<div className={`${style.buttonContainer} ${!showButton && !showMenu ? style.hidden : ''}`}>
<button onClick={toggleMenu} aria-label='toggle menu' className={style.navButton}>
<IoApps />
@@ -92,9 +99,17 @@ export default function NavigationMenu() {
Flip Screen
<IoSwapVertical />
</div>
{/*<div className={style.link} tabIndex={0}>*/}
{/* Rename Client*/}
{/*</div>*/}
<div
className={style.link}
tabIndex={0}
role='button'
onClick={onOpen}
onKeyDown={(event) => {
isKeyEnter(event) && onOpen();
}}
>
Rename Client
</div>
</div>
<hr className={style.separator} />
{navigatorConstants.map((route) => (
@@ -116,3 +131,5 @@ export default function NavigationMenu() {
document.body,
);
}
export default memo(NavigationMenu);
@@ -0,0 +1,5 @@
.modalBody {
display: flex;
flex-direction: column;
gap: 1rem;
}
@@ -0,0 +1,73 @@
import { useEffect, useState } from 'react';
import {
Button,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
} from '@chakra-ui/react';
import { setClientName } from '../../../hooks/useSocket';
import { useSocketClientName } from '../../../stores/connectionName';
import style from './RenameClientModal.module.scss';
interface RenameClientModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function RenameClientModal({ isOpen, onClose }: RenameClientModalProps) {
const { name: clientName, persistName } = useSocketClientName();
const [newName, setNewName] = useState(clientName);
useEffect(() => {
setNewName(clientName);
}, [isOpen, clientName]);
const handleRename = async () => {
if (newName) {
await setClientName(newName);
persistName(newName);
onClose();
}
};
return (
<Modal
isOpen={isOpen}
onClose={onClose}
size='sm'
closeOnOverlayClick={false}
motionPreset='slideInBottom'
scrollBehavior='inside'
preserveScrollBarGap
variant='ontime-small'
>
<ModalOverlay />
<ModalContent>
<ModalHeader>Rename client</ModalHeader>
<ModalCloseButton />
<ModalBody className={style.modalBody}>
<Input
placeholder='Connection must have a name'
defaultValue={newName}
onChange={(e) => setNewName(e.target.value)}
variant='ontime-filled-on-light'
/>
<Button
isDisabled={newName === clientName || !newName}
onClick={handleRename}
width='100%'
variant='ontime-filled'
>
Save
</Button>
</ModalBody>
</ModalContent>
</Modal>
);
}
@@ -105,3 +105,5 @@ export const useTimer = () => {
return useRuntimeStore(featureSelector, deepCompare);
};
export const setClientName = (newName: string) => socketSendJson('set-client-name', newName);
@@ -0,0 +1,26 @@
import { useStore } from 'zustand';
import { createStore } from 'zustand/vanilla';
interface SocketClientNameState {
name?: string;
setName: (newValue: string) => void;
persistName: (newValue: string) => void;
}
const clientNameKey = 'ontime-client-name';
function persistKeyToStorage(newValue: string) {
localStorage.setItem(clientNameKey, newValue);
}
export const socketClientName = createStore<SocketClientNameState>((set) => ({
name: localStorage.getItem(clientNameKey) ?? undefined,
setName: (newValue: string) => set(() => ({ name: newValue })),
persistName: (newValue: string) =>
set(() => {
persistKeyToStorage(newValue);
return { name: newValue };
}),
}));
export const useSocketClientName = () => useStore(socketClientName);
+10 -1
View File
@@ -2,6 +2,7 @@ import { Log, RuntimeStore } from 'ontime-types';
import { RUNTIME, websocketUrl } from '../api/apiConstants';
import { ontimeQueryClient } from '../queryClient';
import { socketClientName } from '../stores/connectionName';
import { addLog } from '../stores/logger';
import { runtime } from '../stores/runtime';
@@ -11,13 +12,17 @@ const reconnectInterval = 1000;
export let shouldReconnect = true;
export let hasConnected = false;
export let reconnectAttempts = 0;
export const connectSocket = () => {
export const connectSocket = (preferredClientName?: string) => {
websocket = new WebSocket(websocketUrl);
websocket.onopen = () => {
clearTimeout(reconnectTimeout as NodeJS.Timeout);
hasConnected = true;
reconnectAttempts = 0;
if (preferredClientName) {
socketSendJson('set-client-name', preferredClientName);
}
};
websocket.onclose = () => {
@@ -49,6 +54,10 @@ export const connectSocket = () => {
// TODO: implement partial store updates
switch (type) {
case 'client-name': {
socketClientName.getState().setName(payload);
break;
}
case 'ontime-log': {
addLog(payload as Log);
break;
+38 -13
View File
@@ -45,9 +45,9 @@ export class SocketServer implements IAdapter {
this.wss = new WebSocketServer({ path: '/ws', server });
this.wss.on('connection', (ws) => {
const clientId = getRandomName();
let clientId = getRandomName();
this.clientIds.add(clientId);
logger.info('RX', `${this.wss.clients.size} Connections with new: ${clientId}`);
logger.info('CLIENT', `${this.wss.clients.size} Connections with new: ${clientId}`);
// send store payload on connect
ws.send(
@@ -57,10 +57,17 @@ export class SocketServer implements IAdapter {
}),
);
ws.send(
JSON.stringify({
type: 'client-name',
payload: clientId,
}),
);
ws.on('error', console.error);
ws.on('close', () => {
logger.info('RX', `${this.wss.clients.size} Connections with disconnected: ${clientId}`);
logger.info('CLIENT', `${this.wss.clients.size} Connections with disconnected: ${clientId}`);
this.clientIds.delete(clientId);
});
@@ -69,20 +76,37 @@ export class SocketServer implements IAdapter {
ws.close();
}
// TODO: protocol specific stuff should be handled here
// eg: rename-client
// socket.on('rename-client', (newName) => {
// if (newName) {
// const previousName = this._clientNames[socket.id];
// this._clientNames[socket.id] = newName;
// this.info('CLIENT', `Client ${previousName} renamed to ${newName}`);
// }
// });
try {
const message = JSON.parse(data);
const { type, payload } = message;
if (type === 'get-client-name') {
ws.send(
JSON.stringify({
type: 'client-name',
payload: clientId,
}),
);
return;
}
if (type === 'set-client-name') {
if (payload) {
const previousName = clientId;
clientId = payload;
this.clientIds.delete(previousName);
this.clientIds.add(clientId);
logger.info('CLIENT', `Client ${previousName} renamed to ${clientId}`);
}
ws.send(
JSON.stringify({
type: 'client-name',
payload: clientId,
}),
);
return;
}
if (type === 'hello') {
ws.send('hi');
return;
@@ -95,6 +119,7 @@ export class SocketServer implements IAdapter {
return;
}
// Protocol specific stuff handled above
try {
const reply = dispatchFromAdapter(type, payload, 'ws');
if (reply) {