feat: allow redirecting to preset

This commit is contained in:
Carlos Valente
2025-02-21 14:34:57 +01:00
committed by Carlos Valente
parent 88b8e83494
commit b8d7324be0
8 changed files with 152 additions and 64 deletions
@@ -0,0 +1,31 @@
.textEntry {
grid-area: input;
display: flex;
align-items: center;
gap: 0.5rem;
color: $label-gray;
}
.inlineEntry {
display: grid;
grid-template-areas:
'label label'
'input btn';
grid-template-columns: 1fr auto;
column-gap: 0.5rem;
}
.redirect {
grid-area: btn;
}
.label {
color: $label-gray;
}
.link {
color: $label-gray;
font-size: $text-body-size;
text-decoration: underline;
text-underline-offset: 2px;
}
@@ -1,64 +1,118 @@
import { useState } from 'react'; import { useState } from 'react';
import { import {
Button, IconButton,
Input, Input,
InputGroup,
InputLeftAddon,
Modal, Modal,
ModalBody, ModalBody,
ModalCloseButton, ModalCloseButton,
ModalContent, ModalContent,
ModalFooter,
ModalHeader, ModalHeader,
ModalOverlay, ModalOverlay,
Select,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { IoArrowForward } from '@react-icons/all-files/io5/IoArrowForward';
import { setClientRemote } from '../../hooks/useSocket'; import { setClientRemote } from '../../hooks/useSocket';
import useUrlPresets from '../../hooks-query/useUrlPresets';
import Info from '../info/Info';
import style from './RedirectClientModal.module.scss';
interface RedirectClientModalProps { interface RedirectClientModalProps {
id: string; id: string;
name?: string; name: string;
path?: string; currentPath: string;
origin: string;
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
} }
export function RedirectClientModal(props: RedirectClientModalProps) { export function RedirectClientModal(props: RedirectClientModalProps) {
const { id, isOpen, name = '', path: currentPath = '', onClose } = props; const { id, isOpen, name, currentPath, origin, onClose } = props;
const { data } = useUrlPresets();
const [path, setPath] = useState(currentPath); const [path, setPath] = useState(currentPath);
const [selected, setSelected] = useState('/');
const { setRedirect } = setClientRemote; const { setRedirect } = setClientRemote;
const handleRedirect = () => { const handleRedirect = (newPath: string) => {
if (path !== currentPath && path !== '') { if (newPath === '/' || newPath === currentPath) {
setRedirect({ target: id, redirect: path }); return;
} }
setRedirect({ target: id, redirect: newPath });
onClose(); onClose();
}; };
const host = window.location.origin; const enabledPresets = data.filter((preset) => preset.enabled);
const canSubmit = path !== currentPath && path !== '';
return ( return (
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'> <Modal isOpen={isOpen} onClose={onClose} variant='ontime'>
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent maxWidth='max(480px, 35vw)'>
<ModalHeader>Redirect: {name}</ModalHeader> <ModalHeader>Redirect: {name}</ModalHeader>
<ModalCloseButton /> <ModalCloseButton />
<ModalBody> <ModalBody>
<InputGroup variant='ontime-filled' size='md'> <Info>
<InputLeftAddon>{host}</InputLeftAddon> Remotely redirect the client to a different URL. <br />
<Input placeholder='minimal?key=0000ffff' value={path} onChange={(event) => setPath(event.target.value)} /> Either by selecting a URL Preset or entering a custom path.
</InputGroup> <br />
<br />
<a href='/editor?settings=feature_settings__urlpresets' target='_blank' className={style.link}>
Manage URL Presets
</a>
</Info>
<div>
<span className={style.label}>Select URL Preset</span>
<div className={style.textEntry}>
<Select
size='md'
variant='ontime'
isDisabled={enabledPresets.length === 0}
onChange={(event) => setSelected(`/${event.target.value}`)}
>
<option value=''>Select a preset</option>
{enabledPresets.map((preset) => {
return (
<option key={preset.pathAndParams} value={preset.pathAndParams}>
{preset.alias}
</option>
);
})}
</Select>
<IconButton
variant='ontime-filled'
size='md'
aria-label='Redirect to preset'
className={style.redirect}
icon={<IoArrowForward />}
isDisabled={enabledPresets.length === 0 || selected === '/'}
onClick={() => handleRedirect(selected)}
/>
</div>
</div>
<div className={style.inlineEntry}>
<span className={style.label}>Enter custom path</span>
<label className={style.textEntry}>
{origin}
<Input
variant='ontime-filled'
size='md'
placeholder='eg. /minimal?key=0000ffff'
value={path}
onChange={(event) => setPath(event.target.value)}
/>
</label>
<IconButton
variant='ontime-filled'
size='md'
aria-label='Redirect'
isDisabled={path === currentPath || path === ''}
className={style.redirect}
icon={<IoArrowForward />}
onClick={() => handleRedirect(path)}
/>
</div>
</ModalBody> </ModalBody>
<ModalFooter>
<Button size='md' variant='ontime-subtle' onClick={onClose}>
Cancel
</Button>
<Button size='md' variant='ontime-filled' onClick={handleRedirect} isDisabled={!canSubmit}>
Submit
</Button>
</ModalFooter>
</ModalContent> </ModalContent>
</Modal> </Modal>
); );
@@ -13,6 +13,7 @@
} }
svg { svg {
min-width: 1.5rem;
align-self: start; align-self: start;
font-size: 1.5rem; font-size: 1.5rem;
color: $info-blue; color: $info-blue;
+15 -13
View File
@@ -34,12 +34,16 @@ export const connectSocket = () => {
hasConnected = true; hasConnected = true;
reconnectAttempts = 0; reconnectAttempts = 0;
socketSendJson('set-client-patch', {
type: 'ontime',
origin: window.location.origin,
path: window.location.pathname + window.location.search,
});
setOnlineStatus(true);
if (preferredClientName) { if (preferredClientName) {
socketSendJson('set-client-name', preferredClientName); socketSendJson('set-client-name', preferredClientName);
} }
socketSendJson('set-client-type', 'ontime');
setOnlineStatus(true);
}; };
websocket.onclose = () => { websocket.onclose = () => {
@@ -78,16 +82,14 @@ export const connectSocket = () => {
updateDevTools({ ping: offset }); updateDevTools({ ping: offset });
break; break;
} }
case 'client-id': { case 'client': {
if (typeof payload === 'string') { if (typeof payload === 'object' || payload !== null) {
setClientId(payload); if (payload.clientId && payload.clientName) {
} setClientId(payload.clientId);
break; if (!preferredClientName) {
} setClientName(payload.clientName);
}
case 'client-name': { }
if (typeof payload === 'string') {
setClientName(payload);
} }
break; break;
} }
@@ -1,5 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { Badge, Button, useDisclosure } from '@chakra-ui/react'; import { Badge, Button, useDisclosure } from '@chakra-ui/react';
import { Client } from 'ontime-types';
import { RedirectClientModal } from '../../../../common/components/client-modal/RedirectClientModal'; import { RedirectClientModal } from '../../../../common/components/client-modal/RedirectClientModal';
import { RenameClientModal } from '../../../../common/components/client-modal/RenameClientModal'; import { RenameClientModal } from '../../../../common/components/client-modal/RenameClientModal';
@@ -31,15 +32,16 @@ export default function ClientList() {
const ontimeClients = Object.entries(clients).filter(([_, { type }]) => type === 'ontime'); const ontimeClients = Object.entries(clients).filter(([_, { type }]) => type === 'ontime');
const otherClients = Object.entries(clients).filter(([_, { type }]) => type !== 'ontime'); const otherClients = Object.entries(clients).filter(([_, { type }]) => type !== 'ontime');
const targetClient = clients[targetId]; const targetClient: Client | undefined = clients[targetId];
return ( return (
<> <>
{isOpenRedirect && ( {isOpenRedirect && targetClient !== undefined && (
<RedirectClientModal <RedirectClientModal
id={targetId} id={targetId}
name={targetClient?.name} name={targetClient.name}
path={targetClient?.path} origin={targetClient.origin}
currentPath={targetClient.path}
isOpen={isOpenRedirect} isOpen={isOpenRedirect}
onClose={onCloseRedirect} onClose={onCloseRedirect}
/> />
@@ -52,7 +54,7 @@ export default function ClientList() {
<Panel.Table> <Panel.Table>
<thead> <thead>
<tr> <tr>
<td className={style.halfWidth}>Client Name (Connection ID)</td> <td className={style.halfWidth}>Client Name</td>
<td className={style.fullWidth}>Path</td> <td className={style.fullWidth}>Path</td>
<td /> <td />
</tr> </tr>
@@ -64,9 +66,6 @@ export default function ClientList() {
return ( return (
<tr key={key}> <tr key={key}>
<Panel.InlineElements relation='inner' as='td'> <Panel.InlineElements relation='inner' as='td'>
<Badge variant='outline' size='xs'>
{key}
</Badge>
{isCurrent && ( {isCurrent && (
<Badge variant='outline' colorScheme='yellow' size='xs'> <Badge variant='outline' colorScheme='yellow' size='xs'>
self self
@@ -119,7 +118,7 @@ export default function ClientList() {
<Panel.Table> <Panel.Table>
<thead> <thead>
<tr> <tr>
<td className={style.halfWidthNoWrap}>Client Name (Connection ID)</td> <td className={style.halfWidthNoWrap}>Client Name</td>
<td className={style.halfWidthNoWrap}>Client type</td> <td className={style.halfWidthNoWrap}>Client type</td>
</tr> </tr>
</thead> </thead>
@@ -129,12 +128,7 @@ export default function ClientList() {
return ( return (
<tr key={key}> <tr key={key}>
<Panel.InlineElements relation='inner' as='td'> <td>{name}</td>
<Badge variant='outline' size='sx'>
{key}
</Badge>
{name}
</Panel.InlineElements>
<td>{type}</td> <td>{type}</td>
</tr> </tr>
); );
+15 -10
View File
@@ -59,11 +59,13 @@ export class SocketServer implements IAdapter {
} }
}); });
const clientId = generateId(); const clientId = generateId();
const clientName = getRandomName();
this.clients.set(clientId, { this.clients.set(clientId, {
type: 'unknown', type: 'unknown',
identify: false, identify: false,
name: getRandomName(), name: clientName,
origin: '',
path: '', path: '',
}); });
@@ -72,15 +74,11 @@ export class SocketServer implements IAdapter {
ws.send( ws.send(
JSON.stringify({ JSON.stringify({
type: 'client-id', type: 'client',
payload: clientId, payload: {
}), clientId,
); clientName,
},
ws.send(
JSON.stringify({
type: 'client-name',
payload: this.clients.get(clientId).name,
}), }),
); );
@@ -128,6 +126,13 @@ export class SocketServer implements IAdapter {
return; return;
} }
if (type === 'set-client-patch') {
if (payload && typeof payload == 'object') {
this.clients.set(clientId, { ...this.clients.get(clientId), ...payload });
}
return;
}
if (type === 'set-client-type') { if (type === 'set-client-type') {
if (payload && typeof payload == 'string') { if (payload && typeof payload == 'string') {
const previousData = this.clients.get(clientId); const previousData = this.clients.get(clientId);
+1 -1
View File
@@ -10,7 +10,7 @@ test('redirect', async ({ context }) => {
await controllerPage.getByTestId('not-self-redirect').click(); await controllerPage.getByTestId('not-self-redirect').click();
await controllerPage.getByPlaceholder('minimal?key=0000ffff').click(); await controllerPage.getByPlaceholder('minimal?key=0000ffff').click();
await controllerPage.getByPlaceholder('minimal?key=0000ffff').fill('clock'); await controllerPage.getByPlaceholder('minimal?key=0000ffff').fill('clock');
await controllerPage.getByRole('button', { name: 'Submit' }).click(); await controllerPage.getByLabel('Redirect', { exact: true }).click();
await expect(remotePage.getByTestId('clock-view')).toBeVisible(); await expect(remotePage.getByTestId('clock-view')).toBeVisible();
}); });
@@ -4,6 +4,7 @@ export type Client = {
name: string; name: string;
type: ClientType; type: ClientType;
identify: boolean; identify: boolean;
origin: string;
path: string; path: string;
}; };