mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-05 23:43:57 +00:00
refactor: remove legacy service
This commit is contained in:
committed by
Carlos Valente
parent
1f71d4578c
commit
2cc434b0e9
@@ -1,21 +0,0 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { HttpSettings } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
|
||||
const httpPath = `${apiEntryUrl}/http`;
|
||||
|
||||
/**
|
||||
* HTTP request to retrieve http settings
|
||||
*/
|
||||
export async function getHTTP(): Promise<HttpSettings> {
|
||||
const res = await axios.get(httpPath);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to mutate http settings
|
||||
*/
|
||||
export async function postHTTP(data: HttpSettings): Promise<AxiosResponse<HttpSettings>> {
|
||||
return axios.post(httpPath, data);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
|
||||
const oscPath = `${apiEntryUrl}/osc`;
|
||||
|
||||
/**
|
||||
* HTTP request to retrieve osc settings
|
||||
*/
|
||||
export async function getOSC(): Promise<OSCSettings> {
|
||||
const res = await axios.get(oscPath);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to mutate osc settings
|
||||
*/
|
||||
export async function postOSC(data: OSCSettings): Promise<AxiosResponse<OSCSettings>> {
|
||||
return axios.post(oscPath, data);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { HTTP_SETTINGS } from '../api/constants';
|
||||
import { getHTTP, postHTTP } from '../api/http';
|
||||
import { logAxiosError } from '../api/utils';
|
||||
import { httpPlaceholder } from '../models/Http';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
|
||||
export function useHttpSettings() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
queryKey: HTTP_SETTINGS,
|
||||
queryFn: getHTTP,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt: number) => attempt * 2500,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return { data: data ?? httpPlaceholder, status, isFetching, isError, refetch };
|
||||
}
|
||||
|
||||
export function usePostHttpSettings() {
|
||||
const { isPending, mutateAsync } = useMutation({
|
||||
mutationFn: postHTTP,
|
||||
onError: (error) => logAxiosError('Error saving HTTP settings', error),
|
||||
onSuccess: (res) => {
|
||||
ontimeQueryClient.setQueryData(HTTP_SETTINGS, res.data);
|
||||
},
|
||||
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: HTTP_SETTINGS }),
|
||||
});
|
||||
return { isPending, mutateAsync };
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { OSC_SETTINGS } from '../api/constants';
|
||||
import { getOSC, postOSC } from '../api/osc';
|
||||
import { logAxiosError } from '../api/utils';
|
||||
import { oscPlaceholderSettings } from '../models/OscSettings';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
|
||||
export default function useOscSettings() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
queryKey: OSC_SETTINGS,
|
||||
queryFn: getOSC,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt: number) => attempt * 2500,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return { data: data ?? oscPlaceholderSettings, status, isFetching, isError, refetch };
|
||||
}
|
||||
|
||||
export function useOscSettingsMutation() {
|
||||
const { isPending, mutateAsync } = useMutation({
|
||||
mutationFn: postOSC,
|
||||
onError: (error) => logAxiosError('Error saving OSC settings', error),
|
||||
onSuccess: (res) => {
|
||||
ontimeQueryClient.setQueryData(OSC_SETTINGS, res.data);
|
||||
},
|
||||
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: OSC_SETTINGS }),
|
||||
});
|
||||
return { isPending, mutateAsync };
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { HttpSettings } from 'ontime-types';
|
||||
|
||||
export const httpPlaceholder: HttpSettings = {
|
||||
enabledOut: false,
|
||||
subscriptions: [],
|
||||
};
|
||||
@@ -1,11 +1,8 @@
|
||||
import { GetInfo } from 'ontime-types';
|
||||
|
||||
import { oscPlaceholderSettings } from './OscSettings';
|
||||
|
||||
export const ontimePlaceholderInfo: GetInfo = {
|
||||
networkInterfaces: [],
|
||||
version: '2.0.0',
|
||||
serverPort: 4001,
|
||||
osc: oscPlaceholderSettings,
|
||||
publicDir: '',
|
||||
};
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
|
||||
export const oscPlaceholderSettings: OSCSettings = {
|
||||
portIn: 8888,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
enabledIn: false,
|
||||
enabledOut: false,
|
||||
subscriptions: [],
|
||||
};
|
||||
@@ -7,7 +7,6 @@ import AutomationPanel from './panel/automations-panel/AutomationPanel';
|
||||
import ClientControlPanel from './panel/client-control-panel/ClientControlPanel';
|
||||
import FeatureSettingsPanel from './panel/feature-settings-panel/FeatureSettingsPanel';
|
||||
import GeneralPanel from './panel/general-panel/GeneralPanel';
|
||||
import IntegrationsPanel from './panel/integrations-panel/IntegrationsPanel';
|
||||
import NetworkLogPanel from './panel/network-panel/NetworkLogPanel';
|
||||
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
||||
import ShutdownPanel from './panel/shutdown-panel/ShutdownPanel';
|
||||
@@ -31,7 +30,6 @@ export default function AppSettings() {
|
||||
{panel === 'general' && <GeneralPanel location={location} />}
|
||||
{panel === 'feature_settings' && <FeatureSettingsPanel location={location} />}
|
||||
{panel === 'sources' && <SourcesPanel />}
|
||||
{panel === 'integrations' && <IntegrationsPanel location={location} />}
|
||||
{panel === 'automation' && <AutomationPanel location={location} />}
|
||||
{panel === 'client_control' && <ClientControlPanel />}
|
||||
{panel === 'about' && <AboutPanel />}
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
||||
import { Button, IconButton, Input, Select, Switch } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
import { HttpSettings } from 'ontime-types';
|
||||
import { generateId } from 'ontime-utils';
|
||||
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import { useHttpSettings, usePostHttpSettings } from '../../../../common/hooks-query/useHttpSettings';
|
||||
import { isKeyEscape } from '../../../../common/utils/keyEvent';
|
||||
import { startsWithHttp } from '../../../../common/utils/regex';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
import { cycles } from './integrationUtils';
|
||||
|
||||
import style from './IntegrationsPanel.module.css';
|
||||
|
||||
export default function HttpIntegrations() {
|
||||
const { data, status } = useHttpSettings();
|
||||
const { mutateAsync } = usePostHttpSettings();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
register,
|
||||
setError,
|
||||
formState: { errors, isSubmitting, isDirty, isValid },
|
||||
} = useForm<HttpSettings>({
|
||||
mode: 'onChange',
|
||||
defaultValues: data,
|
||||
values: data,
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { fields, prepend, remove } = useFieldArray({
|
||||
name: 'subscriptions',
|
||||
control,
|
||||
});
|
||||
|
||||
const onSubmit = async (values: HttpSettings) => {
|
||||
try {
|
||||
await mutateAsync(values);
|
||||
} catch (error) {
|
||||
setError('root', { message: maybeAxiosError(error) });
|
||||
}
|
||||
};
|
||||
|
||||
const preventEscape = (event: React.KeyboardEvent) => {
|
||||
if (isKeyEscape(event)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddNewSubscription = () => {
|
||||
prepend({
|
||||
id: generateId(),
|
||||
cycle: 'onLoad',
|
||||
message: '',
|
||||
enabled: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteSubscription = (index: number) => {
|
||||
remove(index);
|
||||
};
|
||||
|
||||
const canSubmit = !isSubmitting && isDirty && isValid;
|
||||
const isLoading = status === 'pending';
|
||||
|
||||
return (
|
||||
<Panel.Section>
|
||||
<Panel.Card>
|
||||
<Panel.SubHeader>
|
||||
HTTP settings
|
||||
<div className={style.flex}>
|
||||
<Button variant='ontime-ghosted' size='sm' onClick={() => reset()} isDisabled={!canSubmit}>
|
||||
Revert to saved
|
||||
</Button>
|
||||
<Button
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
type='submit'
|
||||
form='http-form'
|
||||
isDisabled={!canSubmit}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</Panel.SubHeader>
|
||||
<Panel.Divider />
|
||||
<Panel.Section as='form' id='http-form' onSubmit={handleSubmit(onSubmit)} onKeyDown={preventEscape}>
|
||||
<Panel.Loader isLoading={isLoading} />
|
||||
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||
<Panel.ListGroup>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='HTTP Output' description='Provide feedback from Ontime through HTTP' />
|
||||
<Controller
|
||||
control={control}
|
||||
name='enabledOut'
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
||||
)}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
|
||||
<Panel.Divider />
|
||||
|
||||
<Panel.Title>
|
||||
HTTP Integration
|
||||
<Button variant='ontime-subtle' size='sm' rightIcon={<IoAdd />} onClick={handleAddNewSubscription}>
|
||||
New
|
||||
</Button>
|
||||
</Panel.Title>
|
||||
{fields.length > 0 && (
|
||||
<Panel.Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Enabled</th>
|
||||
<th>Cycle</th>
|
||||
<th className={style.fullWidth}>Message</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{fields.map((integration, index) => {
|
||||
// @ts-expect-error -- not sure why it is not finding the type, it is ok
|
||||
const maybeError = errors.subscriptions?.[index]?.message?.message;
|
||||
return (
|
||||
<tr key={integration.id}>
|
||||
<td>
|
||||
<Switch variant='ontime' {...register(`subscriptions.${index}.enabled`)} />
|
||||
</td>
|
||||
<td className={style.autoWidth}>
|
||||
<Select
|
||||
size='sm'
|
||||
variant='ontime'
|
||||
className={style.fitContents}
|
||||
{...register(`subscriptions.${index}.cycle`)}
|
||||
>
|
||||
{cycles.map((cycle) => (
|
||||
<option key={cycle.id} value={cycle.value}>
|
||||
{cycle.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</td>
|
||||
<td className={style.fullWidth}>
|
||||
<Input
|
||||
size='sm'
|
||||
variant='ontime-filled'
|
||||
autoComplete='off'
|
||||
placeholder='http://third-party/vt1/{{timer.current}}'
|
||||
{...register(`subscriptions.${index}.message`, {
|
||||
required: { value: true, message: 'Required field' },
|
||||
pattern: {
|
||||
value: startsWithHttp,
|
||||
message: 'HTTP messages should start with http://',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{maybeError && <Panel.Error>{maybeError}</Panel.Error>}
|
||||
</td>
|
||||
<td>
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='ontime-ghosted'
|
||||
color='#FA5656' // $red-500
|
||||
icon={<IoTrash />}
|
||||
aria-label='Delete entry'
|
||||
onClick={() => handleDeleteSubscription(index)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Panel.Table>
|
||||
)}
|
||||
</Panel.Section>
|
||||
</Panel.Card>
|
||||
</Panel.Section>
|
||||
);
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.halfWidth {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.fitContents.fitContents {
|
||||
width: max-content; /* override chakra */
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react';
|
||||
|
||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
import HttpIntegrations from './HttpIntegrations';
|
||||
import OscIntegrations from './OscIntegrations';
|
||||
|
||||
const integrationDocsUrl = 'https://docs.getontime.no/api/integrations/';
|
||||
|
||||
export default function IntegrationsPanel({ location }: PanelBaseProps) {
|
||||
const oscRef = useScrollIntoView<HTMLDivElement>('osc', location);
|
||||
const httpRef = useScrollIntoView<HTMLDivElement>('http', location);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel.Header>Integration settings</Panel.Header>
|
||||
<Panel.Section>
|
||||
<Alert status='info' variant='ontime-on-dark-info'>
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
Integrations allow Ontime to receive commands or send its data to other systems in your workflow. <br />
|
||||
<br />
|
||||
Currently supported protocols are OSC (Open Sound Control), HTTP and Websockets. <br />
|
||||
WebSockets are used for Ontime and cannot be configured independently. <br />
|
||||
<ExternalLink href={integrationDocsUrl}>See the docs</ExternalLink>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</Panel.Section>
|
||||
<Panel.Section>
|
||||
<div ref={oscRef}>
|
||||
<OscIntegrations />
|
||||
</div>
|
||||
<div ref={httpRef}>
|
||||
<HttpIntegrations />
|
||||
</div>
|
||||
</Panel.Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
||||
import { Button, IconButton, Input, Select, Switch } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
import { generateId } from 'ontime-utils';
|
||||
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import useOscSettings, { useOscSettingsMutation } from '../../../../common/hooks-query/useOscSettings';
|
||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import { isASCII, isASCIIorEmpty, isIPAddress, isOnlyNumbers, startsWithSlash } from '../../../../common/utils/regex';
|
||||
import { isOntimeCloud } from '../../../../externals';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
import { cycles } from './integrationUtils';
|
||||
|
||||
import style from './IntegrationsPanel.module.css';
|
||||
|
||||
export default function OscIntegrations() {
|
||||
const { data, status } = useOscSettings();
|
||||
const { mutateAsync } = useOscSettingsMutation();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
register,
|
||||
setError,
|
||||
formState: { errors, isSubmitting, isDirty, isValid },
|
||||
} = useForm<OSCSettings>({
|
||||
mode: 'onChange',
|
||||
defaultValues: data,
|
||||
values: data,
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { fields, prepend, remove } = useFieldArray({
|
||||
name: 'subscriptions',
|
||||
control,
|
||||
});
|
||||
|
||||
// update form if we get new data from server
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset(data);
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
const onSubmit = async (values: OSCSettings) => {
|
||||
if (values.portIn === values.portOut) {
|
||||
setError('portIn', { message: 'OSC IN and OUT Ports cant be the same' });
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedValues = { ...values, portIn: Number(values.portIn), portOut: Number(values.portOut) };
|
||||
try {
|
||||
await mutateAsync(parsedValues);
|
||||
} catch (error) {
|
||||
setError('root', { message: maybeAxiosError(error) });
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddNewSubscription = () => {
|
||||
prepend({
|
||||
id: generateId(),
|
||||
cycle: 'onLoad',
|
||||
address: '',
|
||||
payload: '',
|
||||
enabled: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteSubscription = (index: number) => {
|
||||
remove(index);
|
||||
};
|
||||
|
||||
const canSubmit = !isSubmitting && isDirty && isValid;
|
||||
const isLoading = status === 'pending';
|
||||
|
||||
return (
|
||||
<Panel.Card>
|
||||
<Panel.SubHeader>
|
||||
OSC settings
|
||||
<div className={style.flex}>
|
||||
<Button variant='ontime-ghosted' size='sm' onClick={() => reset()} isDisabled={!canSubmit}>
|
||||
Revert to saved
|
||||
</Button>
|
||||
<Button
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
type='submit'
|
||||
form='osc-form'
|
||||
isDisabled={!canSubmit}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</Panel.SubHeader>
|
||||
{isOntimeCloud && (
|
||||
<Panel.Highlight>For security reasons OSC integrations are not available in the cloud service.</Panel.Highlight>
|
||||
)}
|
||||
|
||||
<Panel.Divider />
|
||||
|
||||
<Panel.Section as='form' id='osc-form' onSubmit={handleSubmit(onSubmit)} onKeyDown={preventEscape}>
|
||||
<Panel.Loader isLoading={isLoading} />
|
||||
<Panel.Title>General OSC settings</Panel.Title>
|
||||
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||
<Panel.ListGroup>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='OSC input' description='Allow control of Ontime through OSC' />
|
||||
<Controller
|
||||
control={control}
|
||||
name='enabledIn'
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
||||
)}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Listen on port'
|
||||
description='Port for incoming OSC. Default: 8888'
|
||||
error={errors.portIn?.message}
|
||||
/>
|
||||
<Input
|
||||
id='portIn'
|
||||
placeholder='8888'
|
||||
width='5rem'
|
||||
maxLength={5}
|
||||
size='sm'
|
||||
textAlign='right'
|
||||
variant='ontime-filled'
|
||||
type='number'
|
||||
autoComplete='off'
|
||||
{...register('portIn', {
|
||||
required: { value: true, message: 'Required field' },
|
||||
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
|
||||
min: { value: 1024, message: 'Port must be within range 1024 - 65535' },
|
||||
pattern: {
|
||||
value: isOnlyNumbers,
|
||||
message: 'Value should be numeric',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
<Panel.ListGroup>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='OSC output' description='Provide feedback from Ontime with OSC' />
|
||||
<Controller
|
||||
control={control}
|
||||
name='enabledOut'
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
||||
)}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='OSC target IP'
|
||||
description='IP address Ontime will send OSC messages to'
|
||||
error={errors.targetIP?.message}
|
||||
/>
|
||||
<Input
|
||||
id='targetIP'
|
||||
placeholder='127.0.0.1'
|
||||
width='9rem'
|
||||
size='sm'
|
||||
textAlign='right'
|
||||
variant='ontime-filled'
|
||||
autoComplete='off'
|
||||
{...register('targetIP', {
|
||||
required: { value: true, message: 'Required field' },
|
||||
pattern: {
|
||||
value: isIPAddress,
|
||||
message: 'Invalid IP address',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='OSC target port'
|
||||
description='Port number Ontime will send OSC messages to'
|
||||
error={errors.portOut?.message}
|
||||
/>
|
||||
<Input
|
||||
id='portOut'
|
||||
placeholder='8888'
|
||||
width='75px'
|
||||
size='sm'
|
||||
textAlign='right'
|
||||
variant='ontime-filled'
|
||||
autoComplete='off'
|
||||
{...register('portOut', {
|
||||
required: { value: true, message: 'Required field' },
|
||||
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
|
||||
min: { value: 1024, message: 'Port must be within range 1024 - 65535' },
|
||||
pattern: {
|
||||
value: isOnlyNumbers,
|
||||
message: 'Value should be numeric',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
|
||||
<Panel.Divider />
|
||||
|
||||
<Panel.Title>
|
||||
OSC integrations
|
||||
<Button variant='ontime-subtle' size='sm' rightIcon={<IoAdd />} onClick={handleAddNewSubscription}>
|
||||
Add
|
||||
</Button>
|
||||
</Panel.Title>
|
||||
|
||||
{fields.length > 0 && (
|
||||
<Panel.Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Enabled</th>
|
||||
<th>Cycle</th>
|
||||
<th className={style.halfWidth}>Address</th>
|
||||
<th className={style.halfWidth}>Arguments</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{fields.map((field, index) => {
|
||||
const maybeAddressError = errors.subscriptions?.[index]?.address?.message;
|
||||
const maybePayloadError = errors.subscriptions?.[index]?.payload?.message;
|
||||
return (
|
||||
<tr key={field.id}>
|
||||
<td>
|
||||
<Switch variant='ontime' {...register(`subscriptions.${index}.enabled`)} />
|
||||
</td>
|
||||
<td className={style.autoWidth}>
|
||||
<Select
|
||||
size='sm'
|
||||
variant='ontime'
|
||||
className={style.fitContents}
|
||||
{...register(`subscriptions.${index}.cycle`)}
|
||||
>
|
||||
{cycles.map((cycle) => (
|
||||
<option key={cycle.id} value={cycle.value}>
|
||||
{cycle.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</td>
|
||||
<td className={style.halfWidth}>
|
||||
<Input
|
||||
key={field.id}
|
||||
size='sm'
|
||||
variant='ontime-filled'
|
||||
autoComplete='off'
|
||||
placeholder='/from-ontime/'
|
||||
{...register(`subscriptions.${index}.address`, {
|
||||
required: { value: true, message: 'Required field' },
|
||||
validate: {
|
||||
oscStartsWithSlash: (value) =>
|
||||
startsWithSlash.test(value) || 'OSC address should start with a forward slash',
|
||||
oscStringIsAscii: (value) =>
|
||||
isASCII.test(value) || 'OSC address only allow ASCII characters',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{maybeAddressError && <Panel.Error>{maybeAddressError}</Panel.Error>}
|
||||
</td>
|
||||
<td className={style.halfWidth}>
|
||||
<Input
|
||||
key={field.id}
|
||||
size='sm'
|
||||
variant='ontime-filled'
|
||||
autoComplete='off'
|
||||
placeholder='{{timer.current}}'
|
||||
{...register(`subscriptions.${index}.payload`, {
|
||||
validate: {
|
||||
oscStringIsAscii: (value) =>
|
||||
isASCIIorEmpty.test(value) || 'OSC arguments only allow ASCII characters',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{maybePayloadError && <Panel.Error>{maybePayloadError}</Panel.Error>}
|
||||
</td>
|
||||
<td>
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='ontime-ghosted'
|
||||
color='#FA5656' // $red-500
|
||||
icon={<IoTrash />}
|
||||
aria-label='Delete entry'
|
||||
onClick={() => handleDeleteSubscription(index)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Panel.Table>
|
||||
)}
|
||||
</Panel.Section>
|
||||
</Panel.Card>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
type CycleLabel = {
|
||||
id: number;
|
||||
label: string;
|
||||
value: keyof typeof TimerLifeCycle;
|
||||
};
|
||||
|
||||
export const cycles: CycleLabel[] = [
|
||||
{ id: 1, label: 'On Load', value: 'onLoad' },
|
||||
{ id: 2, label: 'On Start', value: 'onStart' },
|
||||
{ id: 3, label: 'On Pause', value: 'onPause' },
|
||||
{ id: 4, label: 'On Stop', value: 'onStop' },
|
||||
{ id: 5, label: 'Every second', value: 'onClock' },
|
||||
{ id: 5, label: 'On Timer Update', value: 'onUpdate' },
|
||||
{ id: 6, label: 'On Finish', value: 'onFinish' },
|
||||
{ id: 7, label: 'On Warning', value: 'onWarning' },
|
||||
{ id: 8, label: 'On Danger', value: 'onDanger' },
|
||||
];
|
||||
@@ -23,8 +23,7 @@ type ProjectMergeFormValues = {
|
||||
rundown: boolean;
|
||||
viewSettings: boolean;
|
||||
urlPresets: boolean;
|
||||
osc: boolean;
|
||||
http: boolean;
|
||||
automation: boolean;
|
||||
};
|
||||
|
||||
export default function ProjectMergeForm(props: ProjectMergeFromProps) {
|
||||
@@ -42,8 +41,7 @@ export default function ProjectMergeForm(props: ProjectMergeFromProps) {
|
||||
rundown: false,
|
||||
viewSettings: false,
|
||||
urlPresets: false,
|
||||
osc: false,
|
||||
http: false,
|
||||
automation: false,
|
||||
},
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
@@ -115,12 +113,8 @@ export default function ProjectMergeForm(props: ProjectMergeFromProps) {
|
||||
URL Presets
|
||||
</label>
|
||||
<label>
|
||||
<Switch variant='ontime' {...register('osc')} />
|
||||
OSC Integration
|
||||
</label>
|
||||
<label>
|
||||
<Switch variant='ontime' {...register('http')} />
|
||||
HTTP Integration
|
||||
<Switch variant='ontime' {...register('automation')} />
|
||||
Automation Settings
|
||||
</label>
|
||||
</Panel.Section>
|
||||
</Panel.Section>
|
||||
|
||||
@@ -46,14 +46,6 @@ const staticOptions = [
|
||||
],
|
||||
split: true,
|
||||
},
|
||||
{
|
||||
id: 'integrations',
|
||||
label: 'Integrations',
|
||||
secondary: [
|
||||
{ id: 'integrations__osc', label: 'OSC settings' },
|
||||
{ id: 'integrations__http', label: 'HTTP settings' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'automation',
|
||||
label: 'Automation',
|
||||
|
||||
@@ -3,11 +3,40 @@ import { DatabaseModel, AutomationSettings, Automation, NormalisedAutomationBlue
|
||||
import { dbModel } from '../../models/dataModel.js';
|
||||
import type { ErrorEmitter } from '../../utils/parser.js';
|
||||
|
||||
export function parseAutomationSettings(data: Partial<DatabaseModel>, emitError?: ErrorEmitter): AutomationSettings {
|
||||
interface LegacyData extends Partial<DatabaseModel> {
|
||||
http?: unknown;
|
||||
osc?: {
|
||||
enabledIn?: boolean;
|
||||
portIn?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function parseAutomationSettings(data: LegacyData, emitError?: ErrorEmitter): AutomationSettings {
|
||||
/**
|
||||
* Leaving a path for migrating users to the new automations
|
||||
* This should be removed after a few releases
|
||||
*/
|
||||
if (data.http || data.osc) {
|
||||
emitError?.('Found legacy integrations');
|
||||
console.log('Found legacy integrations...');
|
||||
if (data.osc) {
|
||||
return {
|
||||
enabledAutomations: dbModel.automation.enabledAutomations,
|
||||
enabledOscIn: data.osc?.enabledIn ?? dbModel.automation.enabledOscIn,
|
||||
oscPortIn: data.osc?.portIn ?? dbModel.automation.oscPortIn,
|
||||
automations: [],
|
||||
blueprints: {},
|
||||
};
|
||||
} else {
|
||||
return { ...dbModel.automation };
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.automation) {
|
||||
emitError?.('No data found to import');
|
||||
return { ...dbModel.automation };
|
||||
}
|
||||
console.log('Found Automation settings, importing...');
|
||||
|
||||
return {
|
||||
enabledAutomations: data.automation.enabledAutomations ?? dbModel.automation.enabledAutomations,
|
||||
|
||||
@@ -18,7 +18,7 @@ import * as projectService from '../../services/project-service/ProjectService.j
|
||||
|
||||
export async function patchPartialProjectFile(req: Request, res: Response<DatabaseModel | ErrorResponse>) {
|
||||
try {
|
||||
const { rundown, project, settings, viewSettings, urlPresets, customFields, osc, http, automation } = req.body;
|
||||
const { rundown, project, settings, viewSettings, urlPresets, customFields, automation } = req.body;
|
||||
const patchDb: DatabaseModel = {
|
||||
rundown,
|
||||
project,
|
||||
@@ -26,8 +26,6 @@ export async function patchPartialProjectFile(req: Request, res: Response<Databa
|
||||
viewSettings,
|
||||
urlPresets,
|
||||
customFields,
|
||||
osc,
|
||||
http,
|
||||
automation,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { ErrorResponse, HttpSettings } from 'ontime-types';
|
||||
import { getErrorMessage } from 'ontime-utils';
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
||||
import { httpIntegration } from '../../services/integration-service/HttpIntegration.js';
|
||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
|
||||
export async function getHTTP(_req: Request, res: Response<HttpSettings>) {
|
||||
const http = getDataProvider().getHttp();
|
||||
res.status(200).send(http);
|
||||
}
|
||||
|
||||
export async function postHTTP(req: Request, res: Response<HttpSettings | ErrorResponse>) {
|
||||
if (failEmptyObjects(req.body, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const httpSettings = req.body;
|
||||
|
||||
httpIntegration.init(httpSettings);
|
||||
// we persist the data after init to avoid persisting invalid data
|
||||
const result = await getDataProvider().setHttp(httpSettings);
|
||||
res.send(result).status(200);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(400).send({ message });
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import express from 'express';
|
||||
|
||||
import { validateHTTP } from './http.validation.js';
|
||||
import { getHTTP, postHTTP } from './http.controller.js';
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
router.get('/', getHTTP);
|
||||
router.post('/', validateHTTP, postHTTP);
|
||||
@@ -1,21 +0,0 @@
|
||||
import { body, validationResult } from 'express-validator';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
import { sanitiseHttpSubscriptions } from '../../utils/parserFunctions.js';
|
||||
|
||||
/**
|
||||
* @description Validates object for POST /ontime/http
|
||||
*/
|
||||
export const validateHTTP = [
|
||||
body('enabledOut').exists().isBoolean(),
|
||||
body('subscriptions')
|
||||
.exists()
|
||||
.isArray()
|
||||
.custom((value) => sanitiseHttpSubscriptions(value)),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
@@ -4,8 +4,6 @@ import { router as automationsRouter } from './automation/automation.router.js';
|
||||
import { router as urlPresetsRouter } from './url-presets/urlPresets.router.js';
|
||||
import { router as customFieldsRouter } from './custom-fields/customFields.router.js';
|
||||
import { router as dbRouter } from './db/db.router.js';
|
||||
import { router as httpRouter } from './http/http.router.js';
|
||||
import { router as oscRouter } from './osc/osc.router.js';
|
||||
import { router as projectRouter } from './project/project.router.js';
|
||||
import { router as rundownRouter } from './rundown/rundown.router.js';
|
||||
import { router as settingsRouter } from './settings/settings.router.js';
|
||||
@@ -19,8 +17,6 @@ export const appRouter = express.Router();
|
||||
appRouter.use('/automations', automationsRouter);
|
||||
appRouter.use('/custom-fields', customFieldsRouter);
|
||||
appRouter.use('/db', dbRouter);
|
||||
appRouter.use('/http', httpRouter);
|
||||
appRouter.use('/osc', oscRouter);
|
||||
appRouter.use('/project', projectRouter);
|
||||
appRouter.use('/rundown', rundownRouter);
|
||||
appRouter.use('/settings', settingsRouter);
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { ErrorResponse, OSCSettings } from 'ontime-types';
|
||||
import { getErrorMessage } from 'ontime-utils';
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
||||
import { oscIntegration } from '../../services/integration-service/OscIntegration.js';
|
||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
|
||||
export async function getOSC(_req: Request, res: Response<OSCSettings>) {
|
||||
const osc = getDataProvider().getOsc();
|
||||
res.status(200).send(osc);
|
||||
}
|
||||
|
||||
export async function postOSC(req: Request, res: Response<OSCSettings | ErrorResponse>) {
|
||||
if (failEmptyObjects(req.body, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const oscSettings = req.body;
|
||||
|
||||
oscIntegration.init(oscSettings);
|
||||
// we persist the data after init to avoid persisting invalid data
|
||||
const result = await getDataProvider().setOsc(oscSettings);
|
||||
|
||||
res.send(result).status(200);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(400).send({ message });
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import express from 'express';
|
||||
import { getOSC, postOSC } from './osc.controller.js';
|
||||
import { validateOSC } from './osc.validation.js';
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
router.get('/', getOSC);
|
||||
router.post('/', validateOSC, postOSC);
|
||||
@@ -1,25 +0,0 @@
|
||||
import { body, validationResult } from 'express-validator';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
import { sanitiseOscSubscriptions } from '../../utils/parserFunctions.js';
|
||||
|
||||
/**
|
||||
* @description Validates object for POST /ontime/osc
|
||||
*/
|
||||
export const validateOSC = [
|
||||
body('portIn').exists().isPort(),
|
||||
body('portOut').exists().isPort(),
|
||||
body('targetIP').exists().isIP(),
|
||||
body('enabledIn').exists().isBoolean(),
|
||||
body('enabledOut').exists().isBoolean(),
|
||||
body('subscriptions')
|
||||
.exists()
|
||||
.isArray()
|
||||
.custom((value) => sanitiseOscSubscriptions(value)),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
@@ -34,7 +34,6 @@ export async function getSessionStats(): Promise<SessionStats> {
|
||||
*/
|
||||
export async function getInfo(): Promise<GetInfo> {
|
||||
const { version, serverPort } = getDataProvider().getSettings();
|
||||
const osc = getDataProvider().getOsc();
|
||||
|
||||
// get nif and inject localhost
|
||||
const ni = getNetworkInterfaces();
|
||||
@@ -44,7 +43,6 @@ export async function getInfo(): Promise<GetInfo> {
|
||||
networkInterfaces: ni,
|
||||
version,
|
||||
serverPort,
|
||||
osc,
|
||||
publicDir: publicDir.root,
|
||||
};
|
||||
}
|
||||
|
||||
+2
-33
@@ -27,10 +27,7 @@ import { socket } from './adapters/WebsocketAdapter.js';
|
||||
import { getDataProvider } from './classes/data-provider/DataProvider.js';
|
||||
|
||||
// Services
|
||||
import { integrationService } from './services/integration-service/IntegrationService.js';
|
||||
import { logger } from './classes/Logger.js';
|
||||
import { oscIntegration } from './services/integration-service/OscIntegration.js';
|
||||
import { httpIntegration } from './services/integration-service/HttpIntegration.js';
|
||||
import { populateStyles } from './setup/loadStyles.js';
|
||||
import { eventStore } from './stores/EventStore.js';
|
||||
import { runtimeService } from './services/runtime-service/RuntimeService.js';
|
||||
@@ -134,7 +131,7 @@ let expressServer: Server | null = null;
|
||||
const checkStart = (currentState: OntimeStartOrder) => {
|
||||
if (step !== currentState) {
|
||||
step = OntimeStartOrder.Error;
|
||||
throw new Error('Init order error: initAssets > startServer > startOsc > startIntegrations');
|
||||
throw new Error('Init order error: initAssets > startServer');
|
||||
} else {
|
||||
if (step === 1 || step === 2) {
|
||||
step = step + 1;
|
||||
@@ -234,34 +231,6 @@ export const startServer = async (
|
||||
*/
|
||||
export const startIntegrations = async () => {
|
||||
checkStart(OntimeStartOrder.InitIO);
|
||||
|
||||
// if a config is not provided, we use the persisted one
|
||||
const { osc, http } = getDataProvider().getData();
|
||||
|
||||
if (http) {
|
||||
logger.info(LogOrigin.Tx, 'Initialising HTTP Integration...');
|
||||
try {
|
||||
httpIntegration.init(http);
|
||||
integrationService.register(httpIntegration);
|
||||
} catch (error) {
|
||||
logger.error(LogOrigin.Tx, `HTTP Integration initialisation failed: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (isOntimeCloud) {
|
||||
logger.info(LogOrigin.Tx, 'Skipping OSC in Cloud environment...');
|
||||
return;
|
||||
}
|
||||
|
||||
if (osc) {
|
||||
logger.info(LogOrigin.Tx, 'Initialising OSC Integration...');
|
||||
try {
|
||||
oscIntegration.init(osc);
|
||||
integrationService.register(oscIntegration);
|
||||
} catch (error) {
|
||||
logger.error(LogOrigin.Tx, 'OSC Integration initialisation failed');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -283,8 +252,8 @@ export const shutdown = async (exitCode = 0) => {
|
||||
|
||||
expressServer?.close();
|
||||
runtimeService.shutdown();
|
||||
integrationService.shutdown();
|
||||
logger.shutdown();
|
||||
oscServer.shutdown();
|
||||
socket.shutdown();
|
||||
process.exit(exitCode);
|
||||
};
|
||||
|
||||
@@ -3,10 +3,8 @@ import {
|
||||
OntimeRundown,
|
||||
ViewSettings,
|
||||
DatabaseModel,
|
||||
OSCSettings,
|
||||
Settings,
|
||||
CustomFields,
|
||||
HttpSettings,
|
||||
URLPreset,
|
||||
AutomationSettings,
|
||||
} from 'ontime-types';
|
||||
@@ -49,14 +47,10 @@ export function getDataProvider() {
|
||||
setRundown,
|
||||
getSettings,
|
||||
setSettings,
|
||||
getOsc,
|
||||
getHttp,
|
||||
getUrlPresets,
|
||||
setUrlPresets,
|
||||
getViewSettings,
|
||||
setViewSettings,
|
||||
setOsc,
|
||||
setHttp,
|
||||
getAutomation,
|
||||
setAutomation,
|
||||
getRundown,
|
||||
@@ -104,14 +98,6 @@ async function setSettings(newData: Settings): ReadonlyPromise<Settings> {
|
||||
return db.data.settings;
|
||||
}
|
||||
|
||||
function getOsc(): Readonly<OSCSettings> {
|
||||
return db.data.osc;
|
||||
}
|
||||
|
||||
function getHttp(): Readonly<HttpSettings> {
|
||||
return db.data.http;
|
||||
}
|
||||
|
||||
function getUrlPresets(): Readonly<URLPreset[]> {
|
||||
return db.data.urlPresets;
|
||||
}
|
||||
@@ -132,19 +118,6 @@ async function setViewSettings(newData: ViewSettings): ReadonlyPromise<ViewSetti
|
||||
return db.data.viewSettings;
|
||||
}
|
||||
|
||||
async function setOsc(newData: OSCSettings): ReadonlyPromise<OSCSettings> {
|
||||
db.data.osc = { ...newData };
|
||||
await persist();
|
||||
return db.data.osc;
|
||||
}
|
||||
|
||||
async function setHttp(newData: HttpSettings): ReadonlyPromise<HttpSettings> {
|
||||
db.data.http = { ...newData };
|
||||
|
||||
await persist();
|
||||
return db.data.http;
|
||||
}
|
||||
|
||||
function getAutomation(): Readonly<AutomationSettings> {
|
||||
return db.data.automation;
|
||||
}
|
||||
@@ -165,8 +138,6 @@ async function mergeIntoData(newData: Partial<DatabaseModel>): ReadonlyPromise<D
|
||||
db.data.settings = mergedData.settings;
|
||||
db.data.viewSettings = mergedData.viewSettings;
|
||||
db.data.automation = mergedData.automation;
|
||||
db.data.osc = mergedData.osc;
|
||||
db.data.http = mergedData.http;
|
||||
db.data.urlPresets = mergedData.urlPresets;
|
||||
db.data.customFields = mergedData.customFields;
|
||||
db.data.rundown = mergedData.rundown;
|
||||
|
||||
@@ -11,8 +11,6 @@ export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseMode
|
||||
viewSettings = {},
|
||||
urlPresets = existing.urlPresets,
|
||||
customFields = existing.customFields,
|
||||
osc = {},
|
||||
http = {},
|
||||
automation = existing.automation,
|
||||
} = newData;
|
||||
|
||||
|
||||
@@ -35,18 +35,6 @@ describe('safeMerge', () => {
|
||||
lighting: { type: 'string', label: 'lighting', colour: 'red' },
|
||||
vfx: { type: 'string', label: 'vfx', colour: 'blue' },
|
||||
},
|
||||
osc: {
|
||||
portIn: 8888,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
enabledIn: false,
|
||||
enabledOut: false,
|
||||
subscriptions: [],
|
||||
},
|
||||
http: {
|
||||
enabledOut: false,
|
||||
subscriptions: [],
|
||||
},
|
||||
automation: {
|
||||
enabledAutomations: false,
|
||||
enabledOscIn: false,
|
||||
@@ -107,38 +95,6 @@ describe('safeMerge', () => {
|
||||
language: 'pt',
|
||||
});
|
||||
});
|
||||
it('merges the osc key', () => {
|
||||
const newData = {
|
||||
osc: {
|
||||
portIn: 7777,
|
||||
subscriptions: [
|
||||
{
|
||||
id: 'unique',
|
||||
cycle: 'onStart',
|
||||
message: 'new message',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
//@ts-expect-error -- testing partial merge
|
||||
const mergedData = safeMerge(existing, newData);
|
||||
expect(mergedData.osc).toMatchObject({
|
||||
portIn: 7777,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
enabledIn: false,
|
||||
enabledOut: false,
|
||||
subscriptions: [
|
||||
{
|
||||
id: 'unique',
|
||||
cycle: 'onStart',
|
||||
message: 'new message',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge the urlPresets key when present', () => {
|
||||
const existingData = {
|
||||
@@ -167,17 +123,12 @@ describe('safeMerge', () => {
|
||||
} as ViewSettings,
|
||||
urlPresets: [],
|
||||
customFields: {},
|
||||
osc: {
|
||||
portIn: 8888,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
enabledIn: false,
|
||||
enabledOut: false,
|
||||
subscriptions: [],
|
||||
},
|
||||
http: {
|
||||
enabledOut: false,
|
||||
subscriptions: [],
|
||||
automation: {
|
||||
enabledAutomations: false,
|
||||
enabledOscIn: false,
|
||||
oscPortIn: 8000,
|
||||
automations: [],
|
||||
blueprints: {},
|
||||
},
|
||||
} as DatabaseModel;
|
||||
|
||||
|
||||
@@ -31,18 +31,6 @@ export const dbModel: DatabaseModel = {
|
||||
},
|
||||
urlPresets: [],
|
||||
customFields: {},
|
||||
osc: {
|
||||
portIn: 8888,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
enabledIn: false,
|
||||
enabledOut: false,
|
||||
subscriptions: [],
|
||||
},
|
||||
http: {
|
||||
enabledOut: false,
|
||||
subscriptions: [],
|
||||
},
|
||||
automation: {
|
||||
enabledAutomations: true,
|
||||
enabledOscIn: true,
|
||||
|
||||
@@ -450,18 +450,6 @@ export const demoDb: DatabaseModel = {
|
||||
pathAndParams: 'lower?bg=ff2&text=f00&size=0.6&transition=5',
|
||||
},
|
||||
],
|
||||
osc: {
|
||||
portIn: 8888,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
enabledIn: true,
|
||||
enabledOut: false,
|
||||
subscriptions: [],
|
||||
},
|
||||
http: {
|
||||
enabledOut: false,
|
||||
subscriptions: [],
|
||||
},
|
||||
automation: {
|
||||
enabledAutomations: false,
|
||||
enabledOscIn: true,
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import got from 'got';
|
||||
|
||||
import { HttpSettings, HttpSubscription, LogOrigin } from 'ontime-types';
|
||||
|
||||
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
||||
import { parseTemplateNested } from './integrationUtils.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
|
||||
/**
|
||||
* @description Class contains logic towards outgoing HTTP communications
|
||||
* @class
|
||||
*/
|
||||
export class HttpIntegration implements IIntegration<HttpSubscription, HttpSettings> {
|
||||
subscriptions: HttpSubscription[];
|
||||
enabled: boolean;
|
||||
|
||||
constructor() {
|
||||
this.subscriptions = [];
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes httpClient
|
||||
*/
|
||||
init(config: HttpSettings) {
|
||||
const { subscriptions, enabledOut } = config;
|
||||
this.initSubscriptions(subscriptions);
|
||||
this.enabled = enabledOut;
|
||||
}
|
||||
|
||||
initSubscriptions(subscriptions: HttpSubscription[]) {
|
||||
this.subscriptions = subscriptions;
|
||||
}
|
||||
|
||||
dispatch(action: TimerLifeCycleKey, state?: object) {
|
||||
// noop
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.subscriptions.length; i++) {
|
||||
const { cycle, message, enabled } = this.subscriptions[i];
|
||||
if (cycle !== action || !enabled || !message) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsedMessage = parseTemplateNested(message, state || {});
|
||||
this.emit(parsedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
emit(path: string) {
|
||||
got.get(path, { retry: { limit: 0 } }).catch((err) => {
|
||||
logger.warning(LogOrigin.Tx, `HTTP Integration: ${err.message}`);
|
||||
|
||||
if (err.code === 'ECONNREFUSED') {
|
||||
logger.warning(LogOrigin.Tx, `HTTP Integration: '${err.code}' The server refused the connection`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.code === 'ENOTFOUND') {
|
||||
logger.warning(LogOrigin.Tx, `HTTP Integration: '${err.code}' DNS lookup failed`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.code === 'ETIMEDOUT') {
|
||||
logger.warning(LogOrigin.Tx, `HTTP Integration: '${err.code}' The connection timed out`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warning(LogOrigin.Tx, `HTTP Integration: ${err.code}`);
|
||||
});
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
/** shutdown is a no-op here*/
|
||||
}
|
||||
}
|
||||
|
||||
export const httpIntegration = new HttpIntegration();
|
||||
@@ -1,11 +0,0 @@
|
||||
import { TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
export type TimerLifeCycleKey = keyof typeof TimerLifeCycle;
|
||||
|
||||
export default interface IIntegration<T, C> {
|
||||
subscriptions: T[];
|
||||
init: (config: C) => void;
|
||||
dispatch: (action: TimerLifeCycleKey, state?: object) => void;
|
||||
emit: (...args: never[]) => unknown;
|
||||
shutdown: () => void;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { LogOrigin } from 'ontime-types';
|
||||
|
||||
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
||||
import { eventStore } from '../../stores/EventStore.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
|
||||
class IntegrationService {
|
||||
private integrations: IIntegration<unknown, unknown>[];
|
||||
|
||||
constructor() {
|
||||
this.integrations = [];
|
||||
}
|
||||
|
||||
register(integrationService: IIntegration<unknown, unknown>) {
|
||||
this.integrations.push(integrationService);
|
||||
}
|
||||
|
||||
unregister(integrationService: IIntegration<unknown, unknown>) {
|
||||
this.integrations = this.integrations.filter((int) => int !== integrationService);
|
||||
}
|
||||
|
||||
dispatch(action: TimerLifeCycleKey) {
|
||||
const state = eventStore.poll();
|
||||
this.integrations.forEach((integration) => {
|
||||
integration.dispatch(action, state);
|
||||
});
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
logger.info(LogOrigin.Tx, 'Shutdown Integrations');
|
||||
this.integrations.forEach((integration) => {
|
||||
integration.shutdown();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const integrationService = new IntegrationService();
|
||||
@@ -1,147 +0,0 @@
|
||||
import { ArgumentType, Client, Message } from 'node-osc';
|
||||
import { LogOrigin, MaybeNumber, MaybeString, OSCSettings, OscSubscription } from 'ontime-types';
|
||||
|
||||
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
||||
import { parseTemplateNested } from './integrationUtils.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
import { OscServer } from '../../adapters/OscAdapter.js';
|
||||
import { stringToOSCArgs } from '../../utils/oscArgParser.js';
|
||||
|
||||
/**
|
||||
* @description Class contains logic towards outgoing OSC communications
|
||||
* @class
|
||||
*/
|
||||
export class OscIntegration implements IIntegration<OscSubscription, OSCSettings> {
|
||||
protected oscClient: null | Client;
|
||||
protected oscServer: OscServer | null = null;
|
||||
|
||||
subscriptions: OscSubscription[];
|
||||
targetIP: MaybeString;
|
||||
portOut: MaybeNumber;
|
||||
portIn: MaybeNumber;
|
||||
enabledOut: boolean;
|
||||
enabledIn: boolean;
|
||||
|
||||
constructor() {
|
||||
this.oscClient = null;
|
||||
this.subscriptions = [];
|
||||
this.targetIP = null;
|
||||
this.portOut = null;
|
||||
this.portIn = null;
|
||||
this.enabledOut = false;
|
||||
this.enabledIn = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes oscClient
|
||||
*/
|
||||
init(config: OSCSettings) {
|
||||
const { targetIP, portOut, subscriptions, enabledOut, enabledIn, portIn } = config;
|
||||
|
||||
this.initTX(enabledOut, targetIP, portOut, subscriptions);
|
||||
this.initRX(enabledIn, portIn);
|
||||
// return `OSC integration client connected to ${targetIP}:${portOut}`;
|
||||
}
|
||||
|
||||
private initSubscriptions(subscriptions: OscSubscription[]) {
|
||||
this.subscriptions = subscriptions;
|
||||
}
|
||||
|
||||
dispatch(action: TimerLifeCycleKey, state?: object) {
|
||||
// noop
|
||||
if (!this.oscClient || !this.enabledOut) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.subscriptions.length; i++) {
|
||||
const { cycle, address, payload, enabled } = this.subscriptions[i];
|
||||
if (cycle !== action || !enabled || !address) {
|
||||
continue;
|
||||
}
|
||||
const parsedAddress = parseTemplateNested(address, state || {});
|
||||
const parsedPayload = payload ? parseTemplateNested(payload, state || {}) : undefined;
|
||||
const parsedArguments = stringToOSCArgs(parsedPayload);
|
||||
|
||||
try {
|
||||
this.emit(parsedAddress, parsedArguments);
|
||||
} catch (error) {
|
||||
logger.error(LogOrigin.Tx, `OSC Integration: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit(address: string, args: ArgumentType[]) {
|
||||
if (!this.oscClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO: Look into using bundles
|
||||
const message = new Message(address);
|
||||
message.append(args);
|
||||
|
||||
this.oscClient.send(message);
|
||||
}
|
||||
|
||||
private initTX(enabledOut: boolean, targetIP: string, portOut: number, subscriptions: OscSubscription[]) {
|
||||
this.initSubscriptions(subscriptions);
|
||||
|
||||
if (!enabledOut) {
|
||||
this.targetIP = targetIP;
|
||||
this.portOut = portOut;
|
||||
this.enabledOut = enabledOut;
|
||||
this.shutdownTX();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.oscClient && targetIP === this.targetIP && portOut === this.portOut) {
|
||||
// nothing changed that would mean we need a new client
|
||||
return;
|
||||
}
|
||||
|
||||
this.targetIP = targetIP;
|
||||
this.portOut = portOut;
|
||||
this.enabledOut = enabledOut;
|
||||
|
||||
try {
|
||||
this.oscClient = new Client(targetIP, portOut);
|
||||
logger.info(LogOrigin.Tx, `Starting OSC Clint on port: ${portOut}`);
|
||||
} catch (error) {
|
||||
this.oscClient = null;
|
||||
throw new Error(`Failed initialising OSC client: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private initRX(enabledIn: boolean, portIn: number) {
|
||||
if (!enabledIn) {
|
||||
this.shutdownRX();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start OSC Server
|
||||
logger.info(LogOrigin.Rx, `Starting OSC Server on port: ${portIn}`);
|
||||
this.oscServer = new OscServer(portIn);
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
this.shutdownTX();
|
||||
this.shutdownRX();
|
||||
}
|
||||
|
||||
private shutdownTX() {
|
||||
if (this.oscClient) {
|
||||
logger.info(LogOrigin.Tx, 'Shutting down OSC integration');
|
||||
this.oscClient?.close();
|
||||
this.oscClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
private shutdownRX() {
|
||||
if (this.oscServer) {
|
||||
logger.info(LogOrigin.Rx, 'Shutting down OSC integration');
|
||||
this.oscServer?.shutdown();
|
||||
this.oscServer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const oscIntegration = new OscIntegration();
|
||||
@@ -1,169 +0,0 @@
|
||||
import { stringToOSCArgs } from '../../utils/oscArgParser.js';
|
||||
import { parseTemplateNested } from './integrationUtils.js';
|
||||
|
||||
describe('parseTemplateNested()', () => {
|
||||
it('parses string with a single-level variable name', () => {
|
||||
const store = { timer: 10 };
|
||||
const templateString = '/test/{{timer}}';
|
||||
const result = parseTemplateNested(templateString, store);
|
||||
expect(result).toEqual('/test/10');
|
||||
});
|
||||
|
||||
it('parses string with a nested variable name', () => {
|
||||
const store = { timer: { clock: 10 } };
|
||||
const templateString = '/timer/{{timer.clock}}';
|
||||
const result = parseTemplateNested(templateString, store);
|
||||
expect(result).toEqual('/timer/10');
|
||||
});
|
||||
|
||||
it('parses string with multiple variables', () => {
|
||||
const mockState = { test1: 'that', test2: 'this' };
|
||||
const testString = '{{test1}} should replace {{test2}}';
|
||||
const expected = `${mockState.test1} should replace ${mockState.test2}`;
|
||||
|
||||
const result = parseTemplateNested(testString, mockState);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('correctly parses a string without templates', () => {
|
||||
const testString = 'That should replace {test}';
|
||||
|
||||
const result = parseTemplateNested(testString, {});
|
||||
expect(result).toStrictEqual(testString);
|
||||
});
|
||||
|
||||
it('handles scenarios with missing variables', () => {
|
||||
// by failing to provide a value, we give visibility to
|
||||
// potential issues in the given string
|
||||
const mockState = { test1: 'that', test2: 'this' };
|
||||
const testString = '{{test1}} should replace {{test2}}, but not {{test3}}';
|
||||
const expected = `${mockState.test1} should replace ${mockState.test2}, but not {{test3}}`;
|
||||
|
||||
const result = parseTemplateNested(testString, mockState);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseNestedTemplate() -> resolveAliasData()', () => {
|
||||
it('resolves data through callback', () => {
|
||||
const data = {
|
||||
not: {
|
||||
so: {
|
||||
easy: '3',
|
||||
},
|
||||
},
|
||||
};
|
||||
const aliases = {
|
||||
easy: { key: 'not.so.easy', cb: (value: string) => `testing-${value}` },
|
||||
};
|
||||
|
||||
const easyParse = parseTemplateNested('{{human.easy}}', data, aliases);
|
||||
expect(easyParse).toBe('testing-3');
|
||||
});
|
||||
it('handles a mixed operation', () => {
|
||||
const data = {
|
||||
not: {
|
||||
so: {
|
||||
easy: '3',
|
||||
},
|
||||
},
|
||||
other: {
|
||||
value: 42,
|
||||
},
|
||||
};
|
||||
const aliases = {
|
||||
easy: { key: 'not.so.easy', cb: (value: string) => `testing-${value}` },
|
||||
};
|
||||
|
||||
const easyParse = parseTemplateNested('{{other.value}} to {{human.easy}}', data, aliases);
|
||||
expect(easyParse).toBe('42 to testing-3');
|
||||
});
|
||||
it('returns given key when not found', () => {
|
||||
const data = {
|
||||
not: {
|
||||
so: {
|
||||
easy: '3',
|
||||
},
|
||||
},
|
||||
other: {
|
||||
value: 5,
|
||||
},
|
||||
};
|
||||
const aliases = {
|
||||
easy: { key: 'not.so.easy', cb: (value: string) => `testing-${value}` },
|
||||
};
|
||||
|
||||
const easyParse = parseTemplateNested('{{other.value}} to {{human.easy}} {{human.not.found}}', data, aliases);
|
||||
expect(easyParse).toBe('5 to testing-3 {{human.not.found}}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseNestedTemplate() -> stringToOSCArgs()', () => {
|
||||
it('specific osc requirements', () => {
|
||||
const data = {
|
||||
not: {
|
||||
so: {
|
||||
easy: 'data with space',
|
||||
empty: '',
|
||||
number: 1234,
|
||||
stringNumber: '1234',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const payloads = [
|
||||
{
|
||||
test: '"string with space and {{not.so.easy}}"',
|
||||
expect: [{ type: 'string', value: 'string with space and data with space' }],
|
||||
},
|
||||
{
|
||||
test: '',
|
||||
expect: [],
|
||||
},
|
||||
{
|
||||
test: ' ',
|
||||
expect: [],
|
||||
},
|
||||
{
|
||||
test: '""',
|
||||
expect: [{ type: 'string', value: '' }],
|
||||
},
|
||||
{
|
||||
test: '"string with space and {{not.so.empty}}"',
|
||||
expect: [{ type: 'string', value: 'string with space and ' }],
|
||||
},
|
||||
{
|
||||
test: '"string with space and {{not.so.number}}"',
|
||||
expect: [{ type: 'string', value: 'string with space and 1234' }],
|
||||
},
|
||||
{
|
||||
test: '"string with space and {{not.so.stringNumber}}"',
|
||||
expect: [{ type: 'string', value: 'string with space and 1234' }],
|
||||
},
|
||||
{
|
||||
test: '"{{not.so.easy}}" 1',
|
||||
expect: [
|
||||
{ type: 'string', value: 'data with space' },
|
||||
{ type: 'integer', value: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
test: '"{{not.so.empty}}" 1',
|
||||
expect: [
|
||||
{ type: 'string', value: '' },
|
||||
{ type: 'integer', value: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
test: '',
|
||||
expect: [],
|
||||
},
|
||||
];
|
||||
|
||||
payloads.forEach((payload) => {
|
||||
const parsedPayload = parseTemplateNested(payload.test, data);
|
||||
const parsedArguments = stringToOSCArgs(parsedPayload);
|
||||
expect(parsedArguments).toStrictEqual(payload.expect);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import { MaybeNumber } from 'ontime-types';
|
||||
import { millisToString, removeLeadingZero } from 'ontime-utils';
|
||||
|
||||
// any value inside double curly braces {{val}}
|
||||
const placeholderRegex = /{{(.*?)}}/g;
|
||||
|
||||
function formatDisplayFromString(value: string, hideZero = false): string {
|
||||
let valueInNumber: MaybeNumber = null;
|
||||
|
||||
if (value !== 'null') {
|
||||
const parsedValue = Number(value);
|
||||
if (!Number.isNaN(parsedValue)) {
|
||||
valueInNumber = parsedValue;
|
||||
}
|
||||
}
|
||||
let formatted = millisToString(valueInNumber, { fallback: hideZero ? '00:00' : '00:00:00' });
|
||||
if (hideZero) {
|
||||
formatted = removeLeadingZero(formatted);
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
|
||||
type AliasesDefinition = Record<string, { key: string; cb: (value: string) => string }>;
|
||||
const quickAliases: AliasesDefinition = {
|
||||
clock: { key: 'clock', cb: (value: string) => formatDisplayFromString(value) },
|
||||
duration: { key: 'timer.duration', cb: (value: string) => formatDisplayFromString(value, true) },
|
||||
expectedEnd: {
|
||||
key: 'timer.expectedFinish',
|
||||
cb: (value: string) => formatDisplayFromString(value),
|
||||
},
|
||||
runningTimer: {
|
||||
key: 'timer.current',
|
||||
cb: (value: string) => formatDisplayFromString(value, true),
|
||||
},
|
||||
elapsedTime: {
|
||||
key: 'timer.elapsed',
|
||||
cb: (value: string) => formatDisplayFromString(value, true),
|
||||
},
|
||||
startedAt: { key: 'timer.startedAt', cb: (value: string) => formatDisplayFromString(value) },
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a templated string to values in a nested object
|
||||
*/
|
||||
export function parseTemplateNested(template: string, state: object, humanReadable = quickAliases): string {
|
||||
let parsedTemplate = template;
|
||||
const matches = Array.from(parsedTemplate.matchAll(placeholderRegex));
|
||||
|
||||
for (const match of matches) {
|
||||
const variableName = match[1];
|
||||
const variableParts = variableName.split('.');
|
||||
let value: string | undefined = undefined;
|
||||
|
||||
if (variableParts[0] === 'human') {
|
||||
const lookupKey = variableParts[1];
|
||||
if (lookupKey in humanReadable) {
|
||||
const newTemplate = `{{${humanReadable[lookupKey].key}}}`;
|
||||
const parsed = parseTemplateNested(newTemplate, state, humanReadable);
|
||||
value = humanReadable[lookupKey].cb(parsed);
|
||||
} else {
|
||||
value = undefined;
|
||||
}
|
||||
} else {
|
||||
// iterate through variable parts, and look for the property in the state object
|
||||
value = variableParts.reduce((obj, key) => obj?.[key], state);
|
||||
}
|
||||
if (value !== undefined) {
|
||||
parsedTemplate = parsedTemplate.replace(match[0], value);
|
||||
}
|
||||
}
|
||||
|
||||
return parsedTemplate;
|
||||
}
|
||||
@@ -28,8 +28,6 @@ import {
|
||||
setLastLoadedProject,
|
||||
} from '../app-state-service/AppStateService.js';
|
||||
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||
import { oscIntegration } from '../integration-service/OscIntegration.js';
|
||||
import { httpIntegration } from '../integration-service/HttpIntegration.js';
|
||||
|
||||
import {
|
||||
copyCorruptFile,
|
||||
@@ -176,14 +174,10 @@ export async function loadProjectFile(name: string) {
|
||||
// apply data model
|
||||
runtimeService.stop();
|
||||
|
||||
const { rundown, customFields, osc, http } = result.data;
|
||||
const { rundown, customFields } = result.data;
|
||||
|
||||
// apply the rundown
|
||||
await initRundown(rundown, customFields);
|
||||
|
||||
// apply integrations
|
||||
oscIntegration.init(osc);
|
||||
httpIntegration.init(http);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,14 +244,10 @@ export async function renameProjectFile(originalFile: string, newFilename: strin
|
||||
// apply data model
|
||||
runtimeService.stop();
|
||||
|
||||
const { rundown, customFields, osc, http } = result.data;
|
||||
const { rundown, customFields } = result.data;
|
||||
|
||||
// apply the rundown
|
||||
await initRundown(rundown, customFields);
|
||||
|
||||
// apply integrations
|
||||
oscIntegration.init(osc);
|
||||
httpIntegration.init(http);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
getRundown,
|
||||
getTimedEvents,
|
||||
} from '../rundown-service/rundownUtils.js';
|
||||
import { integrationService } from '../integration-service/IntegrationService.js';
|
||||
|
||||
import { getForceUpdate, getShouldClockUpdate, getShouldTimerUpdate } from './rundownService.utils.js';
|
||||
import { skippedOutOfEvent } from '../timerUtils.js';
|
||||
|
||||
@@ -314,7 +314,7 @@ describe('test parser edge cases', () => {
|
||||
//@ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const { data, errors } = parseDatabaseModel(testData);
|
||||
expect(data.rundown.length).toBe(1);
|
||||
expect(errors.length).toBe(7);
|
||||
expect(errors.length).toBe(5);
|
||||
});
|
||||
|
||||
it('handles incomplete datasets', () => {
|
||||
@@ -724,8 +724,6 @@ describe('test import of v2 datamodel', () => {
|
||||
);
|
||||
// @ts-expect-error -- checking if the field is removed
|
||||
expect(parsed?.userFields).toBeUndefined();
|
||||
expect(parsed.osc).toMatchObject({ subscriptions: [] });
|
||||
expect(parsed.http).toMatchObject({ enabledOut: false, subscriptions: [] });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,12 +2,8 @@ import {
|
||||
CustomFields,
|
||||
DatabaseModel,
|
||||
EndAction,
|
||||
HttpSettings,
|
||||
HttpSubscription,
|
||||
OSCSettings,
|
||||
OntimeEvent,
|
||||
OntimeRundown,
|
||||
OscSubscription,
|
||||
Settings,
|
||||
SupportedEvent,
|
||||
TimeStrategy,
|
||||
@@ -17,16 +13,12 @@ import {
|
||||
|
||||
import {
|
||||
parseCustomFields,
|
||||
parseHttp,
|
||||
parseOsc,
|
||||
parseProject,
|
||||
parseRundown,
|
||||
parseSettings,
|
||||
parseUrlPresets,
|
||||
parseViewSettings,
|
||||
sanitiseCustomFields,
|
||||
sanitiseHttpSubscriptions,
|
||||
sanitiseOscSubscriptions,
|
||||
} from '../parserFunctions.js';
|
||||
|
||||
describe('parseRundown()', () => {
|
||||
@@ -111,66 +103,6 @@ describe('parseViewSettings()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseOsc()', () => {
|
||||
it('returns an a base model if nothing is given', () => {
|
||||
const errorEmitter = vi.fn();
|
||||
const result = parseOsc({}, errorEmitter);
|
||||
expect(result).toBeTypeOf('object');
|
||||
expect(errorEmitter).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('parses data, skipping invalid results', () => {
|
||||
const errorEmitter = vi.fn();
|
||||
const osc = {
|
||||
subscriptions: [
|
||||
{ id: '1', cycle: 'onLoad', address: '/test', payload: 'test', enabled: true }, // OK
|
||||
{}, // no data
|
||||
{ id: '2', cycle: 'onStart', payload: 'test', enabled: true }, // no address
|
||||
],
|
||||
} as OSCSettings;
|
||||
const result = parseOsc({ osc }, errorEmitter);
|
||||
expect(result.subscriptions.length).toEqual(1);
|
||||
expect(result.subscriptions.at(0)).toMatchObject({
|
||||
id: '1',
|
||||
cycle: 'onLoad',
|
||||
address: '/test',
|
||||
payload: 'test',
|
||||
enabled: true,
|
||||
});
|
||||
expect(errorEmitter).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseHttp()', () => {
|
||||
it('returns an a base model if nothing is given', () => {
|
||||
const errorEmitter = vi.fn();
|
||||
const result = parseHttp({}, errorEmitter);
|
||||
expect(result).toBeTypeOf('object');
|
||||
expect(errorEmitter).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('parses data, skipping invalid results', () => {
|
||||
const errorEmitter = vi.fn();
|
||||
const http = {
|
||||
subscriptions: [
|
||||
{ id: '1', cycle: 'onLoad', message: 'http://', enabled: true }, // OK
|
||||
{}, // no data
|
||||
{ id: '2', cycle: 'onStart', enabled: true }, // no message
|
||||
{ id: '3', cycle: 'onLoad', message: '/test', enabled: true }, // doesnt start with http
|
||||
],
|
||||
} as HttpSettings;
|
||||
const result = parseHttp({ http }, errorEmitter);
|
||||
expect(result.subscriptions.length).toEqual(1);
|
||||
expect(result.subscriptions.at(0)).toMatchObject({
|
||||
id: '1',
|
||||
cycle: 'onLoad',
|
||||
message: 'http://',
|
||||
enabled: true,
|
||||
});
|
||||
expect(errorEmitter).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseUrlPresets()', () => {
|
||||
it('returns an a base model if nothing is given', () => {
|
||||
const errorEmitter = vi.fn();
|
||||
@@ -223,81 +155,6 @@ describe('parseCustomFields()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitiseOscSubscriptions()', () => {
|
||||
it('throws if not an array an empty array if not an array', () => {
|
||||
expect(() => sanitiseOscSubscriptions(undefined)).toThrow();
|
||||
// @ts-expect-error -- data is external, we check bad types
|
||||
expect(() => sanitiseOscSubscriptions({})).toThrow();
|
||||
expect(() => sanitiseOscSubscriptions(null)).toThrow();
|
||||
});
|
||||
|
||||
it('returns an array of valid entries', () => {
|
||||
const oscSubscriptions: OscSubscription[] = [
|
||||
{ id: '1', cycle: 'onLoad', address: '/test', payload: 'test', enabled: true },
|
||||
{ id: '2', cycle: 'onStart', address: '/test', payload: 'test', enabled: false },
|
||||
{ id: '3', cycle: 'onPause', address: '/test', payload: 'test', enabled: true },
|
||||
{ id: '4', cycle: 'onStop', address: '/test', payload: 'test', enabled: false },
|
||||
{ id: '5', cycle: 'onUpdate', address: '/test', payload: 'test', enabled: true },
|
||||
{ id: '6', cycle: 'onFinish', address: '/test', payload: 'test', enabled: false },
|
||||
{ id: '7', cycle: 'onWarning', address: '/test', payload: 'test', enabled: false },
|
||||
{ id: '8', cycle: 'onDanger', address: '/test', payload: 'test', enabled: false },
|
||||
];
|
||||
const sanitationResult = sanitiseOscSubscriptions(oscSubscriptions);
|
||||
expect(sanitationResult).toStrictEqual(oscSubscriptions);
|
||||
});
|
||||
|
||||
it('filters invalid entries', () => {
|
||||
const oscSubscriptions = [
|
||||
{ id: '1', cycle: 'onLoad', address: 4, payload: 'test', enabled: true },
|
||||
{ cycle: 'onLoad', payload: 'test', enabled: true },
|
||||
{ id: '2', cycle: 'unknown', payload: 'test', enabled: false },
|
||||
{ id: '3', payload: 'test', enabled: true },
|
||||
{ id: '4', cycle: 'onStop', enabled: false },
|
||||
{ id: '5', cycle: 'onUpdate', payload: 'test' },
|
||||
{ id: '6', cycle: 'onFinish', payload: 'test', enabled: 'true' },
|
||||
] as OscSubscription[];
|
||||
const sanitationResult = sanitiseOscSubscriptions(oscSubscriptions);
|
||||
expect(sanitationResult.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitiseHttpSubscriptions()', () => {
|
||||
it('throws if the data is unexpected', () => {
|
||||
expect(() => sanitiseHttpSubscriptions(undefined)).toThrow();
|
||||
// @ts-expect-error -- data is external, we check bad types
|
||||
expect(() => sanitiseHttpSubscriptions({})).toThrow();
|
||||
expect(() => sanitiseHttpSubscriptions(null)).toThrow();
|
||||
});
|
||||
|
||||
it('returns an array of valid entries', () => {
|
||||
const httpSubscription: HttpSubscription[] = [
|
||||
{ id: '1', cycle: 'onLoad', message: 'http://test', enabled: true },
|
||||
{ id: '2', cycle: 'onStart', message: 'http://test', enabled: false },
|
||||
{ id: '3', cycle: 'onPause', message: 'http://test', enabled: true },
|
||||
{ id: '4', cycle: 'onStop', message: 'http://test', enabled: false },
|
||||
{ id: '5', cycle: 'onUpdate', message: 'http://test', enabled: true },
|
||||
{ id: '6', cycle: 'onFinish', message: 'http://test', enabled: false },
|
||||
{ id: '7', cycle: 'onWarning', message: 'http://test', enabled: false },
|
||||
{ id: '8', cycle: 'onDanger', message: 'http://test', enabled: false },
|
||||
];
|
||||
const sanitationResult = sanitiseHttpSubscriptions(httpSubscription);
|
||||
expect(sanitationResult).toStrictEqual(httpSubscription);
|
||||
});
|
||||
|
||||
it('filters invalid entries', () => {
|
||||
const httpSubscription = [
|
||||
{ cycle: 'onLoad', message: 'http://test', enabled: true },
|
||||
{ id: '2', cycle: 'unknown', message: 'http://test', enabled: false },
|
||||
{ id: '3', message: 'http://test', enabled: true },
|
||||
{ id: '4', cycle: 'onStop', enabled: false },
|
||||
{ id: '5', cycle: 'onUpdate', message: 'http://test' },
|
||||
{ id: '6', cycle: 'onFinish', message: 'ftp://test', enabled: 'true' },
|
||||
];
|
||||
const sanitationResult = sanitiseHttpSubscriptions(httpSubscription as HttpSubscription[]);
|
||||
expect(sanitationResult.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitiseCustomFields()', () => {
|
||||
it('returns an empty array if not an array', () => {
|
||||
expect(sanitiseCustomFields({})).toEqual({});
|
||||
|
||||
@@ -27,15 +27,7 @@ import { logger } from '../classes/Logger.js';
|
||||
import { event as eventDef } from '../models/eventsDefinition.js';
|
||||
|
||||
import { makeString } from './parserUtils.js';
|
||||
import {
|
||||
parseHttp,
|
||||
parseOsc,
|
||||
parseProject,
|
||||
parseRundown,
|
||||
parseSettings,
|
||||
parseUrlPresets,
|
||||
parseViewSettings,
|
||||
} from './parserFunctions.js';
|
||||
import { parseProject, parseRundown, parseSettings, parseUrlPresets, parseViewSettings } from './parserFunctions.js';
|
||||
import { parseExcelDate } from './time.js';
|
||||
|
||||
export type ErrorEmitter = (message: string) => void;
|
||||
@@ -334,8 +326,6 @@ export function parseDatabaseModel(jsonData: Partial<DatabaseModel>): { data: Da
|
||||
viewSettings: parseViewSettings(jsonData, makeEmitError('View Settings')),
|
||||
urlPresets: parseUrlPresets(jsonData, makeEmitError('URL Presets')),
|
||||
customFields,
|
||||
osc: parseOsc(jsonData, makeEmitError('OSC')),
|
||||
http: parseHttp(jsonData, makeEmitError('HTTP')),
|
||||
automation: parseAutomationSettings(jsonData),
|
||||
};
|
||||
|
||||
|
||||
@@ -2,25 +2,20 @@ import {
|
||||
CustomField,
|
||||
CustomFields,
|
||||
DatabaseModel,
|
||||
HttpSettings,
|
||||
HttpSubscription,
|
||||
OSCSettings,
|
||||
OntimeBlock,
|
||||
OntimeDelay,
|
||||
OntimeEvent,
|
||||
OntimeRundown,
|
||||
OscSubscription,
|
||||
ProjectData,
|
||||
Settings,
|
||||
TimerType,
|
||||
URLPreset,
|
||||
ViewSettings,
|
||||
isOntimeBlock,
|
||||
isOntimeCycle,
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
} from 'ontime-types';
|
||||
import { customFieldLabelToKey, generateId, getErrorMessage, isAlphanumericWithSpace } from 'ontime-utils';
|
||||
import { customFieldLabelToKey, generateId, isAlphanumericWithSpace } from 'ontime-utils';
|
||||
|
||||
import { dbModel } from '../models/dataModel.js';
|
||||
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
|
||||
@@ -165,102 +160,6 @@ export function parseViewSettings(data: Partial<DatabaseModel>, emitError?: Erro
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitises an OSC Subscriptions array
|
||||
*/
|
||||
export function sanitiseOscSubscriptions(subscriptions?: OscSubscription[]): OscSubscription[] {
|
||||
if (!Array.isArray(subscriptions)) {
|
||||
throw new Error('ERROR: invalid OSC subscriptions');
|
||||
}
|
||||
|
||||
return subscriptions.filter(
|
||||
({ id, cycle, address, payload, enabled }) =>
|
||||
typeof id === 'string' &&
|
||||
isOntimeCycle(cycle) &&
|
||||
typeof address === 'string' &&
|
||||
typeof payload === 'string' &&
|
||||
typeof enabled === 'boolean',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse osc portion of an entry
|
||||
*/
|
||||
export function parseOsc(data: Partial<DatabaseModel>, emitError?: ErrorEmitter): OSCSettings {
|
||||
if (!data.osc) {
|
||||
emitError?.('No data found to import');
|
||||
return { ...dbModel.osc };
|
||||
}
|
||||
|
||||
console.log('Found OSC settings, importing...');
|
||||
|
||||
let newSubscriptions: OscSubscription[] = [];
|
||||
try {
|
||||
newSubscriptions = sanitiseOscSubscriptions(data.osc.subscriptions);
|
||||
} catch (error) {
|
||||
emitError?.(getErrorMessage(error));
|
||||
}
|
||||
|
||||
if (newSubscriptions.length !== data.osc.subscriptions.length) {
|
||||
emitError?.('Skipped invalid subscriptions');
|
||||
}
|
||||
|
||||
return {
|
||||
portIn: data.osc.portIn ?? dbModel.osc.portIn,
|
||||
portOut: data.osc.portOut ?? dbModel.osc.portOut,
|
||||
targetIP: data.osc.targetIP ?? dbModel.osc.targetIP,
|
||||
enabledIn: data.osc.enabledIn ?? dbModel.osc.enabledIn,
|
||||
enabledOut: data.osc.enabledOut ?? dbModel.osc.enabledOut,
|
||||
subscriptions: newSubscriptions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitises an HTTP Subscriptions array
|
||||
*/
|
||||
export function sanitiseHttpSubscriptions(subscriptions?: HttpSubscription[]): HttpSubscription[] {
|
||||
if (!Array.isArray(subscriptions)) {
|
||||
throw new Error('ERROR: invalid HTTP subscriptions');
|
||||
}
|
||||
|
||||
return subscriptions.filter(
|
||||
({ id, cycle, message, enabled }) =>
|
||||
typeof id === 'string' &&
|
||||
isOntimeCycle(cycle) &&
|
||||
typeof message === 'string' &&
|
||||
message.startsWith('http://') &&
|
||||
typeof enabled === 'boolean',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Http portion of an entry
|
||||
*/
|
||||
export function parseHttp(data: Partial<DatabaseModel>, emitError?: ErrorEmitter): HttpSettings {
|
||||
if (!data.http) {
|
||||
emitError?.('No data found to import');
|
||||
return { ...dbModel.http };
|
||||
}
|
||||
|
||||
console.log('Found HTTP settings, importing...');
|
||||
|
||||
let newSubscriptions: HttpSubscription[] = [];
|
||||
try {
|
||||
newSubscriptions = sanitiseHttpSubscriptions(data.http.subscriptions);
|
||||
} catch (error) {
|
||||
emitError?.(getErrorMessage(error));
|
||||
}
|
||||
|
||||
if (newSubscriptions.length !== data.http?.subscriptions.length) {
|
||||
emitError?.('Skipped invalid subscriptions');
|
||||
}
|
||||
|
||||
return {
|
||||
enabledOut: data.http.enabledOut ?? dbModel.http.enabledOut,
|
||||
subscriptions: newSubscriptions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse URL preset portion of an entry
|
||||
*/
|
||||
|
||||
@@ -436,17 +436,12 @@
|
||||
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
||||
}
|
||||
],
|
||||
"osc": {
|
||||
"portIn": 8888,
|
||||
"portOut": 9999,
|
||||
"targetIP": "127.0.0.1",
|
||||
"enabledIn": true,
|
||||
"enabledOut": true,
|
||||
"subscriptions": []
|
||||
},
|
||||
"http": {
|
||||
"enabledOut": true,
|
||||
"subscriptions": []
|
||||
"automation": {
|
||||
"enabledAutomations": false,
|
||||
"enabledOscIn": true,
|
||||
"oscPortIn": 8888,
|
||||
"automations": [],
|
||||
"blueprints": {}
|
||||
},
|
||||
"customFields": {
|
||||
"song": {
|
||||
|
||||
Vendored
+6
-11
@@ -436,17 +436,12 @@
|
||||
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
||||
}
|
||||
],
|
||||
"osc": {
|
||||
"portIn": 8888,
|
||||
"portOut": 9999,
|
||||
"targetIP": "127.0.0.1",
|
||||
"enabledIn": true,
|
||||
"enabledOut": true,
|
||||
"subscriptions": []
|
||||
},
|
||||
"http": {
|
||||
"enabledOut": true,
|
||||
"subscriptions": []
|
||||
"automation": {
|
||||
"enabledAutomations": false,
|
||||
"enabledOscIn": true,
|
||||
"oscPortIn": 8888,
|
||||
"automations": [],
|
||||
"blueprints": {}
|
||||
},
|
||||
"customFields": {
|
||||
"song": {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { OSCSettings } from '../../definitions/core/OscSettings.type.js';
|
||||
import type { OntimeRundown } from '../../definitions/core/Rundown.type.js';
|
||||
import type { Playback } from '../../definitions/runtime/Playback.type.js';
|
||||
import type { MaybeString } from '../../utils/utils.type.js';
|
||||
@@ -22,7 +21,6 @@ export interface GetInfo {
|
||||
networkInterfaces: NetworkInterface[];
|
||||
version: string;
|
||||
serverPort: number;
|
||||
osc: OSCSettings;
|
||||
publicDir: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type {
|
||||
AutomationSettings,
|
||||
CustomFields,
|
||||
HttpSettings,
|
||||
OntimeRundown,
|
||||
OSCSettings,
|
||||
ProjectData,
|
||||
Settings,
|
||||
URLPreset,
|
||||
@@ -17,7 +15,5 @@ export type DatabaseModel = {
|
||||
viewSettings: ViewSettings;
|
||||
urlPresets: URLPreset[];
|
||||
customFields: CustomFields;
|
||||
osc: OSCSettings;
|
||||
http: HttpSettings;
|
||||
automation: AutomationSettings;
|
||||
};
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { TimerLifeCycleKey } from './TimerLifecycle.type.js';
|
||||
|
||||
export type HttpSubscription = { id: string; cycle: TimerLifeCycleKey; message: string; enabled: boolean };
|
||||
|
||||
export interface HttpSettings {
|
||||
enabledOut: boolean;
|
||||
subscriptions: HttpSubscription[];
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { TimerLifeCycleKey } from './TimerLifecycle.type.js';
|
||||
|
||||
export type OscSubscription = {
|
||||
id: string;
|
||||
cycle: TimerLifeCycleKey;
|
||||
address: string;
|
||||
payload: string; // TODO: we should be using arguments to keep in line with protocol language
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export interface OSCSettings {
|
||||
portIn: number;
|
||||
portOut: number;
|
||||
targetIP: string;
|
||||
enabledIn: boolean;
|
||||
enabledOut: boolean;
|
||||
subscriptions: OscSubscription[];
|
||||
}
|
||||
@@ -52,10 +52,6 @@ export type {
|
||||
EventCustomFields,
|
||||
} from './definitions/core/CustomFields.type.js';
|
||||
|
||||
// ---> Integration, Subscription
|
||||
export type { OSCSettings, OscSubscription } from './definitions/core/OscSettings.type.js';
|
||||
export type { HttpSettings, HttpSubscription } from './definitions/core/HttpSettings.type.js';
|
||||
|
||||
// SERVER RESPONSES
|
||||
export type {
|
||||
AuthenticationStatus,
|
||||
|
||||
Reference in New Issue
Block a user