mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-10 08:39:34 +00:00
feat: allow redirecting to preset
This commit is contained in:
committed by
Carlos Valente
parent
88b8e83494
commit
b8d7324be0
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user