V2 integration (#293)

* feat: integrations and lifecycles

* refactor: prevent issues with start order
This commit is contained in:
Carlos Valente
2023-02-23 20:22:38 +01:00
committed by GitHub
parent d8a84ac88d
commit 70cc071425
61 changed files with 1510 additions and 687 deletions
+1
View File
@@ -25,6 +25,7 @@
"react-beautiful-dnd": "^13.1.1",
"react-dom": "^18.2.0",
"react-fast-compare": "^3.2.0",
"react-hook-form": "^7.43.1",
"react-qr-code": "^2.0.11",
"react-router-dom": "^6.3.0",
"react-table": "^7.7.0",
+3 -3
View File
@@ -3,7 +3,7 @@ import axios from 'axios';
import { URLAliasType } from '../models/Alias.type';
import { InfoType } from '../models/Info.types';
import { OntimeSettingsType } from '../models/OntimeSettings.type';
import { OscSettingsType } from '../models/OscSettings.type';
import { OSCSettings } from '../models/OscSettings.type';
import { UserFieldsType } from '../models/UserFields.type';
import { ViewSettingsType } from '../models/ViewSettings.type';
@@ -90,7 +90,7 @@ export async function postUserFields(data: UserFieldsType) {
* @description HTTP request to retrieve osc settings
* @return {Promise}
*/
export async function getOSC(): Promise<OscSettingsType> {
export async function getOSC(): Promise<OSCSettings> {
const res = await axios.get(`${ontimeURL}/osc`);
return res.data;
}
@@ -99,7 +99,7 @@ export async function getOSC(): Promise<OscSettingsType> {
* @description HTTP request to mutate osc settings
* @return {Promise}
*/
export async function postOSC(data: OscSettingsType) {
export async function postOSC(data: OSCSettings) {
return axios.post(`${ontimeURL}/osc`, data);
}
@@ -1,13 +1,16 @@
export type OscSettingsType = {
port: string;
import { OSCSettings } from 'ontime-types';
// in the placeholder, we pass strings to satisfy input type
export interface PlaceholderSettings extends Omit<OSCSettings, 'portIn' | 'portOut'> {
portIn: string;
portOut: string;
targetIP: string;
enabled: boolean;
}
export const oscPlaceholderSettings: OscSettingsType = {
port: '',
export const oscPlaceholderSettings: PlaceholderSettings = {
portIn: '',
portOut: '',
targetIP: '',
enabled: false,
enabledIn: false,
enabledOut: false,
subscriptions: {},
};
@@ -0,0 +1,27 @@
import { isIPAddress, isOnlyNumbers } from '../regex';
describe('simple tests for regex', () => {
test('isOnlyNumbers', () => {
const right = ['1231', '1'];
const wrong = ['a', 'asdas1asdas', '11as', '1_', '1.1'];
right.forEach((t) => {
expect(isOnlyNumbers.test(t)).toBe(true);
});
wrong.forEach((t) => {
expect(isOnlyNumbers.test(t)).toBe(false);
});
});
test('isIPAddress', () => {
const right = ['0.0.0.0', '127.0.0.1'];
const wrong = ['0', 'testing', '123.0.1'];
right.forEach((t) => {
expect(isIPAddress.test(t)).toBe(true);
});
wrong.forEach((t) => {
expect(isIPAddress.test(t)).toBe(false);
});
});
});
+2
View File
@@ -0,0 +1,2 @@
export const isOnlyNumbers = /^\d+$/;
export const isIPAddress = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
+13 -11
View File
@@ -1,10 +1,11 @@
import { lazy, useEffect } from 'react';
import { Box, useDisclosure } from '@chakra-ui/react';
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
import UploadModal from '../../common/components/upload-modal/UploadModal';
import ModalManager from '../../features/modals/ModalManager';
import ModalManager from '../modals/ModalManager';
import MenuBar from '../menu/MenuBar';
import IntegrationModal from '../modals/integration-modal/IntegrationModal';
import styles from './Editor.module.scss';
@@ -15,16 +16,14 @@ const Info = lazy(() => import('../../features/info/InfoExport'));
const EventEditor = lazy(() => import('../../features/event-editor/EventEditorExport'));
export default function Editor() {
const {
isOpen: isSettingsOpen,
onOpen: onSettingsOpen,
onClose: onSettingsClose,
} = useDisclosure();
const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure();
const { isOpen: isUploadModalOpen, onOpen: onUploadModalOpen, onClose: onUploadModalClose } = useDisclosure();
const {
isOpen: isUploadModalOpen,
onOpen: onUploadModalOpen,
onClose: onUploadModalClose,
isOpen: isIntegrationModalOpen,
onOpen: onIntegrationModalOpen,
onClose: onIntegrationModalClose,
} = useDisclosure();
// Set window title
@@ -35,10 +34,11 @@ export default function Editor() {
return (
<>
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
<IntegrationModal onClose={onIntegrationModalClose} isOpen={isIntegrationModalOpen} />
<ErrorBoundary>
<ModalManager isOpen={isSettingsOpen} onClose={onSettingsClose} />
</ErrorBoundary>
<div className={styles.mainContainer} data-testid="event-editor">
<div className={styles.mainContainer} data-testid='event-editor'>
<Box id='settings' className={styles.settings}>
<ErrorBoundary>
<MenuBar
@@ -47,6 +47,8 @@ export default function Editor() {
onSettingsClose={onSettingsClose}
isUploadOpen={isUploadModalOpen}
onUploadOpen={onUploadModalOpen}
isIntegrationOpen={isIntegrationModalOpen}
onIntegrationOpen={onIntegrationModalOpen}
/>
</ErrorBoundary>
</Box>
+52 -28
View File
@@ -4,6 +4,8 @@ import { FiHelpCircle } from '@react-icons/all-files/fi/FiHelpCircle';
import { FiMinimize } from '@react-icons/all-files/fi/FiMinimize';
import { FiSave } from '@react-icons/all-files/fi/FiSave';
import { FiUpload } from '@react-icons/all-files/fi/FiUpload';
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
import { IoScan } from '@react-icons/all-files/io5/IoScan';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
import { downloadRundown } from '../../common/api/ontimeApi';
@@ -20,6 +22,8 @@ interface MenuBarProps {
onSettingsClose: () => void;
isUploadOpen: boolean;
onUploadOpen: () => void;
isIntegrationOpen: boolean;
onIntegrationOpen: () => void;
}
type Actions = 'min' | 'max' | 'shutdown' | 'help';
@@ -29,42 +33,53 @@ const buttonStyle = {
size: 'lg',
colorScheme: 'white',
_hover: {
background: 'rgba(255, 255, 255, 0.10)' // $white-10
background: 'rgba(255, 255, 255, 0.10)', // $white-10
},
_active: {
background: 'rgba(255, 255, 255, 0.13)' // $white-13
}
background: 'rgba(255, 255, 255, 0.13)', // $white-13
},
};
export default function MenuBar(props: MenuBarProps) {
const { isSettingsOpen, onSettingsOpen, onSettingsClose, isUploadOpen, onUploadOpen } = props;
const {
isSettingsOpen,
onSettingsOpen,
onSettingsClose,
isUploadOpen,
onUploadOpen,
isIntegrationOpen,
onIntegrationOpen,
} = props;
const { isElectron, sendToElectron } = useElectronEvent();
const actionHandler = useCallback((action: Actions) => {
// Stop crashes when testing locally
if (!isElectron) {
if (action === 'help') {
window.open('https://cpvalente.gitbook.io/ontime/');
const actionHandler = useCallback(
(action: Actions) => {
// Stop crashes when testing locally
if (!isElectron) {
if (action === 'help') {
window.open('https://cpvalente.gitbook.io/ontime/');
}
} else {
switch (action) {
case 'min':
sendToElectron('set-window', 'to-tray');
break;
case 'max':
sendToElectron('set-window', 'to-max');
break;
case 'shutdown':
sendToElectron('shutdown', 'now');
break;
case 'help':
sendToElectron('send-to-link', 'help');
break;
default:
break;
}
}
} else {
switch (action) {
case 'min':
sendToElectron('set-window', 'to-tray');
break;
case 'max':
sendToElectron('set-window', 'to-max');
break;
case 'shutdown':
sendToElectron('shutdown', 'now');
break;
case 'help':
sendToElectron('send-to-link', 'help');
break;
default:
break;
}
}
}, [sendToElectron, isElectron]);
},
[sendToElectron, isElectron],
);
// Handle keyboard shortcuts
const handleKeyPress = useCallback(
@@ -129,6 +144,15 @@ export default function MenuBar(props: MenuBarProps) {
aria-label='Settings'
/>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={isIntegrationOpen ? <IoExtensionPuzzle /> : <IoExtensionPuzzleOutline />}
className={isIntegrationOpen ? style.open : ''}
clickHandler={onIntegrationOpen}
tooltip='Integrations'
aria-label='Integrations'
/>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={<FiUpload />}
@@ -0,0 +1,77 @@
@use '../../theme/v2Styles' as *;
@use '../../theme/ontimeColours' as *;
.headerNotes {
font-size: $text-body-size;
width: 100%;
padding: 0 16px;
color: $modal-note-color;
margin-bottom: $section-spacing;
}
.divider {
margin: 8px 0;
border: 0;
border-top: 1px solid $gray-100;
}
.sectionContainer {
display: flex;
flex-direction: column;
height: 100%
}
.splitSection {
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 3px;
padding: 8px;
&:hover {
background-color: $blue-50;
}
}
.sectionTitle {
font-size: 14px;
display: block;
&.main {
font-weight: 600;
}
}
@mixin subsection {
font-size: 14px;
display: block;
}
.sectionSubtitle {
@include subsection;
color: $modal-note-color;
}
.error {
@include subsection;
color: $error-red;
}
.buttonSection {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid $gray-100;
display: flex;
gap: 16px;
}
.buttonSection {
button:first-of-type {
margin-right: auto;
}
}
.spacer {
flex-grow: 1;
}
@@ -10,17 +10,19 @@ import {
TabPanels,
Tabs,
} from '@chakra-ui/react';
import PropTypes from 'prop-types';
import AliasesModal from './AliasesModal';
import AppSettingsModal from './AppSettingsModal';
import EventSettingsModal from './EventSettingsModal';
import IntegrationSettingsModal from './IntegrationSettingsModal';
import OscSettingsModal from './OscSettingsModal';
import TableOptionsModal from './TableOptionsModal';
import ViewsSettingsModal from './ViewsSettingsModal';
export default function ModalManager(props) {
interface ModalManagerProps {
isOpen: boolean;
onClose: () => void;
}
export default function ModalManager(props: ModalManagerProps) {
const { isOpen, onClose } = props;
return (
<Modal
@@ -38,13 +40,11 @@ export default function ModalManager(props) {
<Tabs size='sm' isLazy>
<TabList>
<Tab style={{ fontSize: '0.9em' }}>App Settings</Tab>
<Tab style={{ fontSize: '0.9em' }}>Viewers</Tab>
<Tab style={{ fontSize: '0.9em' }}>Event Data</Tab>
<Tab style={{ fontSize: '0.9em' }}>URL Aliases</Tab>
<Tab style={{ fontSize: '0.9em' }}>Cuesheet</Tab>
<Tab style={{ fontSize: '0.9em' }}>OSC</Tab>
{/*<Tab style={{ fontSize: '0.9em' }}>Integration</Tab>*/}
<Tab>App Settings</Tab>
<Tab>Viewers</Tab>
<Tab>Event Data</Tab>
<Tab>URL Aliases</Tab>
<Tab>Cuesheet</Tab>
</TabList>
<TabPanels>
<TabPanel>
@@ -62,20 +62,9 @@ export default function ModalManager(props) {
<TabPanel>
<TableOptionsModal />
</TabPanel>
<TabPanel>
<OscSettingsModal />
</TabPanel>
{/*<TabPanel>*/}
{/* <IntegrationSettingsModal />*/}
{/*</TabPanel>*/}
</TabPanels>
</Tabs>
</ModalContent>
</Modal>
);
}
ModalManager.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
};
@@ -0,0 +1,23 @@
import { Button, ModalFooter } from '@chakra-ui/react';
import styles from './Modal.module.scss';
export default function ModalSubmitFooter() {
return (
<ModalFooter className={styles.buttonSection}>
<Button variant='ghosted' paddingLeft={0} color='#6c6c6c'>
Revert to saved
</Button>
<Button colorScheme='gray'>Cancel</Button>
<Button
variant='ontime-filled'
type='submit'
// disabled={isSubmitting}
isLoading={false}
padding='0 2.5em'
>
Save
</Button>
</ModalFooter>
);
}
@@ -0,0 +1,41 @@
import { PropsWithChildren } from 'react';
import {
Button,
Modal,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from '@chakra-ui/react';
import styles from './Modal.module.scss';
interface ModalWrapperProps {
isOpen: boolean;
onClose: () => void;
title: string;
}
export default function ModalWrapper(props: PropsWithChildren<ModalWrapperProps>) {
const { isOpen, onClose, title, children } = props;
return (
<Modal
onClose={onClose}
isOpen={isOpen}
closeOnOverlayClick={false}
motionPreset='slideInBottom'
size='xl'
scrollBehavior='inside'
preserveScrollBarGap
variant='ontime'
>
<ModalOverlay />
<ModalContent>
<ModalHeader>{title}</ModalHeader>
<ModalCloseButton />
{children}
</ModalContent>
</Modal>
);
}
@@ -1,4 +1,5 @@
@use '../../theme/main' as *;
@use '../../theme/ontimeColours' as *;
@use '../../theme/v2Styles' as *;
//////////////////////////////////// main
@@ -7,7 +8,7 @@
.notes {
font-weight: 400;
color: $light-bg;
color: $action-blue;
display: grid;
place-items: center;
height: 4em;
@@ -16,7 +17,7 @@
.modalFields {
max-height: 45vh;
overflow-y: auto;
scrollbar-color: rgba($light-bg, 0.35) rgba($light-bg, 0.15);
scrollbar-color: rgba($action-blue, 0.35) rgba($action-blue, 0.15);
padding-right: 6px;
label {
@@ -42,7 +43,7 @@
grid-template-columns: 20% 1fr 4em;
.placeholder {
background: $light-text;
background: black;
width: 100%;
height: 24px;
}
@@ -51,19 +52,19 @@
/* Track */
::-webkit-scrollbar-track {
background: rgba($light-bg, 0.15);
background: rgba($gray-50, 0.15);
border-radius: 3px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: rgba($light-bg, 0.35);
background: rgba($gray-100, 0.35);
border-radius: 3px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: rgba($light-bg, 0.45);
background: rgba($gray-200, 0.45);
}
.modalInline {
@@ -99,9 +100,8 @@
padding-top: 2em;
display: flex;
justify-content: flex-end;
gap: 1em;
gap: 16px;
}
}
.modalBody > * {
@@ -115,7 +115,7 @@ ul.featureList {
display: flex;
align-items: center;
svg {
color: $ontime-accent-text;
color: black;
margin-right: 4px;
}
}
@@ -124,7 +124,7 @@ ul.featureList {
p {
&.notes {
text-align: center;
border-color: $light-bg-transparent;
border-color: black;
border-width: 0 2px;
font-size: 0.9em;
margin-bottom: 1em;
@@ -139,7 +139,7 @@ span {
}
.blockNotes {
background-color: $bg-gray;
background-color: $gray-1100;
margin: 1em 0;
padding: 0.5em;
font-size: 0.8em;
@@ -147,7 +147,7 @@ span {
table {
background-color: #fff;
border-left: 4px solid lighten($ontime-pink, 5%);
border-left: 4px solid lighten($ontime-color, 5%);
width: 100%;
margin: 0.5em 0;
border-radius: 2px;
@@ -181,12 +181,12 @@ span {
}
.labelNote {
color: $light-bg;
color: $action-blue;
padding-right: 1em;
}
.labelNoteInline {
color: $light-bg;
color: $action-blue;
}
.inlineFlex {
@@ -0,0 +1,40 @@
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
import ModalWrapper from '../ModalWrapper';
import OscIntegrationSettings from './OscIntegrationSettings';
import OscSettingsModal from './OscSettingsModal';
import styles from '../Modal.module.scss';
interface IntegrationModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function IntegrationModal(props: IntegrationModalProps) {
const { isOpen, onClose } = props;
return (
<ModalWrapper title='Integration Settings' isOpen={isOpen} onClose={onClose}>
<div className={styles.headerNotes}>
Manage settings related to protocol integrations. <br />
Changes take effect on app restart.
</div>
<Tabs variant='ontime' size='sm' isLazy>
<TabList>
<Tab>OSC</Tab>
<Tab>Old OSC</Tab>
</TabList>
<TabPanels>
<TabPanel>
<OscIntegrationSettings />
</TabPanel>
<TabPanel>
<OscSettingsModal />
</TabPanel>
</TabPanels>
</Tabs>
</ModalWrapper>
);
}
@@ -0,0 +1,181 @@
import { useContext } from 'react';
import { useForm } from 'react-hook-form';
import { Button, FormControl, Input, ModalBody, ModalFooter, Switch } from '@chakra-ui/react';
import { postOSC } from '../../../common/api/ontimeApi';
import { LoggingContext } from '../../../common/context/LoggingContext';
import useOscSettings from '../../../common/hooks-query/useOscSettings';
import { PlaceholderSettings } from '../../../common/models/OscSettings.type';
import { isIPAddress, isOnlyNumbers } from '../../../common/utils/regex';
import styles from '../Modal.module.scss';
export default function OscIntegrationSettings() {
const { data } = useOscSettings();
const { emitError } = useContext(LoggingContext);
const {
handleSubmit,
register,
reset,
setError,
formState: { errors, isSubmitting, isDirty, isValid },
} = useForm<PlaceholderSettings>({
defaultValues: data,
values: data,
});
const disableSubmit = isSubmitting || !isDirty || !isValid;
const onSubmit = async (values: PlaceholderSettings) => {
const numericPortIn = Number(values.portIn);
const numericPortOut = Number(values.portOut);
if (numericPortIn === numericPortOut) {
setError('portIn', { message: 'OSC IN and OUT Ports cant be the same' });
return;
}
const parsedValues = {
...values,
portIn: numericPortIn,
portOut: numericPortOut,
};
try {
await postOSC(parsedValues);
} catch (error) {
emitError(`Error setting OSC: ${error}`);
}
};
const resetForm = () => reset(data);
return (
<>
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='test'>
<ModalBody>
<div className={styles.splitSection}>
<div>
<span className={`${styles.sectionTitle} ${styles.main}`}>OSC Input</span>
<span className={styles.sectionSubtitle}>Control Ontime with OSC</span>
</div>
<Switch {...register('enabledIn')} variant='ontime-on-light' />
</div>
<FormControl isInvalid={!!errors.portIn} className={styles.splitSection}>
<label htmlFor='portIn'>
<span className={styles.sectionTitle}>Listen on Port</span>
{errors.portIn ? (
<span className={styles.error}>{errors.portIn.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 8888</span>
)}
</label>
<Input
id='portIn'
placeholder='8888'
width='75px'
size='sm'
textAlign='right'
maxLength={5}
{...register('portIn', {
required: { value: true, message: 'Required field' },
max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' },
min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' },
pattern: {
value: isOnlyNumbers,
message: 'Value should be numeric',
},
})}
/>
</FormControl>
<hr className={styles.divider} />
<div className={styles.splitSection}>
<div>
<span className={styles.sectionTitle} style={{ fontWeight: 600 }}>
OSC Output
</span>
<span className={styles.sectionSubtitle}>Ontime data feedback</span>
</div>
<Switch {...register('enabledOut')} variant='ontime-on-light' />
</div>
<FormControl isInvalid={!!errors.targetIP} className={styles.splitSection}>
<label htmlFor='targetIP'>
<span className={styles.sectionTitle}>OSC target IP</span>
{errors.targetIP ? (
<span className={styles.error}>{errors.targetIP.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 127.0.0.1</span>
)}
</label>
<Input
id='targetIP'
placeholder='127.0.0.1'
width='140px'
size='sm'
textAlign='right'
{...register('targetIP', {
required: { value: true, message: 'Required field' },
pattern: {
value: isIPAddress,
message: 'Invalid IP address',
},
})}
/>
</FormControl>
<FormControl className={styles.splitSection}>
<label htmlFor='portOut'>
<span className={styles.sectionTitle}>OSC target Port</span>
{errors.portOut ? (
<span className={styles.error}>{errors.portOut.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 9999</span>
)}
</label>
<Input
id='portOut'
placeholder='9999'
width='75px'
size='sm'
textAlign='right'
maxLength={5}
{...register('portOut', {
required: { value: true, message: 'Required field' },
max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' },
min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' },
pattern: {
value: isOnlyNumbers,
message: 'Value should be numeric',
},
})}
/>
</FormControl>
</ModalBody>
{/*<ModalSubmitFooter />*/}
</form>
<ModalFooter className={styles.buttonSection}>
<Button variant='ontime-ghost-on-light' size='sm' onClick={resetForm}>
Revert to saved
</Button>
<Button variant='ontime-subtle-on-light' size='sm'>
Cancel
</Button>
<Button
variant='ontime-filled'
type='submit'
form='test'
disabled={disableSubmit}
isLoading={isSubmitting}
padding='0 2em'
size='sm'
>
Save
</Button>
</ModalFooter>
</>
);
}
@@ -2,16 +2,15 @@ import { useCallback, useContext, useEffect, useState } from 'react';
import { FormControl, FormLabel, Input, ModalBody } from '@chakra-ui/react';
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
import { postOSC } from '../../common/api/ontimeApi';
import EnableBtn from '../../common/components/buttons/EnableBtn';
import { LoggingContext } from '../../common/context/LoggingContext';
import useOscSettings from '../../common/hooks-query/useOscSettings';
import { oscPlaceholderSettings } from '../../common/models/OscSettings.type';
import { postOSC } from '../../../common/api/ontimeApi';
import EnableBtn from '../../../common/components/buttons/EnableBtn';
import { LoggingContext } from '../../../common/context/LoggingContext';
import useOscSettings from '../../../common/hooks-query/useOscSettings';
import { oscPlaceholderSettings } from '../../../common/models/OscSettings.type';
import { inputProps, portInputProps } from '../modalHelper';
import SubmitContainer from '../SubmitContainer';
import { inputProps, portInputProps } from './modalHelper';
import SubmitContainer from './SubmitContainer';
import style from './Modals.module.scss';
import style from '../Modals.module.scss';
// currently defined endpoints
// temporary
@@ -9,4 +9,4 @@ export const portInputProps = {
type: 'number',
min: '1024',
max: '65535',
};
};
+7
View File
@@ -7,6 +7,13 @@
scrollbar-width: thin;
}
// reset box-sizing
*,
*:before,
*:after {
box-sizing: inherit;
}
body,
html,
.App {
-5
View File
@@ -2,7 +2,6 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
import { TimerType } from 'ontime-types';
import App from './App';
import { ONTIME_VERSION } from './ONTIME_VERSION';
@@ -20,10 +19,6 @@ Sentry.init({
enabled: import.meta.env.PROD,
});
// TODO: apply code and remove, refs PR #290
const sharedType: TimerType = TimerType.CountDown;
console.log('WIP', sharedType);
root.render(
<StrictMode>
<App />
+2
View File
@@ -35,6 +35,8 @@ $bg-container-onlight: $gray-100;
$box-shadow-l1: rgba(0, 0, 0, 0.15) 0 3px 3px 0;
$box-shadow-l2: rgba(0, 0, 0, 0.15) 0 3px 3px 0;
$modal-note-color: $gray-700;
// interface elements
$border-color-ondark: $white-10;
$element-inner-spacing: 4px;
+29 -9
View File
@@ -1,17 +1,13 @@
const commonStyles = {
letterSpacing: '0.3px',
fontWeight: '400',
borderRadius: '3px',
};
export const ontimeButtonFilled = {
...commonStyles,
background: '#2B5ABC', // $blue-700
color: '#fff', // pure-white
border: '1px solid #2B5ABC', // $blue-700
_hover: {
backgroundColor: '#0A43B9', // $blue-800
border: '1px solid #0A43B9', // $blue-800
_disabled: {
background: '#2B5ABC', // $blue-700
},
},
_active: {
backgroundColor: '#0036A6', // blue-900
@@ -20,7 +16,6 @@ export const ontimeButtonFilled = {
};
export const ontimeButtonOutlined = {
...commonStyles,
backgroundColor: '#2d2d2d', // $gray-1100
color: '#e2e2e2', // $blue-400
border: '1px solid rgba(255, 255, 255, 0.10)', // white-10
@@ -34,7 +29,6 @@ export const ontimeButtonOutlined = {
};
export const ontimeButtonSubtle = {
...commonStyles,
backgroundColor: '#303030', // $gray-1050
color: '#779BE7', // $blue-400
border: '1px solid transparent',
@@ -47,6 +41,32 @@ export const ontimeButtonSubtle = {
},
};
export const ontimeButtonSubtleOnLight = {
backgroundColor: '#ececec', // $gray-100
color: '#595959', // $gray-800
border: '1px solid transparent',
_hover: {
backgroundColor: '#cfcfcf', // $gray-200
},
_active: {
backgroundColor: '#ececec', // $gray-200
borderColor: '#ececec', // $gray-300
},
};
export const ontimeGhostOnLight = {
backgroundColor: 'transparent',
color: '#595959', // $gray-800
_hover: {
color: '#595959', // $gray-800
backgroundColor: '#ececec', // $gray-200
},
_active: {
backgroundColor: 'transparent',
borderColor: '#595959', // $gray-800
},
};
export const ontimeButtonSubtleWhite = {
...ontimeButtonSubtle,
color: '#f6f6f6', // $gray-50
+20
View File
@@ -0,0 +1,20 @@
export const ontimeModal = {
header: {
fontWeight: 400,
letterSpacing: '0.3px',
padding: '8px 16px',
fontSize: '20px',
color: '#202020', // $gray-50
},
dialog: {
borderRadius: '3px',
padding: 0,
minHeight: 'min(500px, 75vh)',
},
body: {
padding: 0,
},
closeButton: {
color: '#202020', // $gray-50
},
};
+14 -3
View File
@@ -1,5 +1,4 @@
export const ontimeSwitch = {
container: { },
track: {
background: '#2d2d2d', // $gray-1100
border: '1px solid transparent',
@@ -8,7 +7,19 @@ export const ontimeSwitch = {
},
_focus: {
border: '1px solid #578AF4', // $blue-500
}
},
},
};
export const lightSwitch = {
track: {
border: '2px solid transparent',
background: '#cfcfcf', // $gray-300
_checked: {
background: `#578AF4`, // $blue-500
},
_focus: {
border: '2px solid #D2DDFF', // $blue-200
},
},
thumb: {},
};
+16
View File
@@ -0,0 +1,16 @@
export const ontimeTab = {
tab: {
fontWeight: 600,
borderBottom: '2px solid transparent',
color: '#9d9d9d', // $gray-500
marginBottom: '-2px',
_selected: {
color: '#101010', // $ui-black
border: 'none',
borderBottom: '2px solid #779BE7', // $blue-400
},
},
tablist: {
borderBottom: '2px solid #ececec', // $gray-100
},
};
+25 -10
View File
@@ -4,24 +4,26 @@ import {
ontimeButtonFilled,
ontimeButtonOutlined,
ontimeButtonSubtle,
ontimeButtonSubtleOnLight,
ontimeButtonSubtleWhite,
ontimeGhostOnLight,
} from './ontimeButton';
import { ontimeCheckboxOnDark } from './ontimeCheckbox';
import { ontimeEditable } from './ontimeEditable';
import { ontimeMenuOnDark } from './ontimeMenu';
import { ontimeModal } from './ontimeModal';
import { ontimeSelect } from './ontimeSelect';
import { ontimeSwitch } from './ontimeSwitch';
import {
ontimeInputFilled,
ontimeTextAreaFilled,
ontimeTextAreaFilledOnLight,
} from './ontimeTextInputs';
import { lightSwitch, ontimeSwitch } from './ontimeSwitch';
import { ontimeTab } from './ontimeTab';
import { ontimeInputFilled, ontimeTextAreaFilled, ontimeTextAreaFilledOnLight } from './ontimeTextInputs';
import { ontimeTooltip } from './ontimeTooltip';
const theme = extendTheme({
components: {
Button: {
baseStyle: {
letterSpacing: '0.3px',
fontWeight: '400',
borderRadius: '3px',
},
variants: {
@@ -29,6 +31,8 @@ const theme = extendTheme({
'ontime-outlined': { ...ontimeButtonOutlined },
'ontime-subtle': { ...ontimeButtonSubtle },
'ontime-subtle-white': { ...ontimeButtonSubtleWhite },
'ontime-subtle-on-light': { ...ontimeButtonSubtleOnLight },
'ontime-ghost-on-light': { ...ontimeGhostOnLight },
},
},
Checkbox: {
@@ -38,7 +42,7 @@ const theme = extendTheme({
},
Editable: {
variants: {
'ontime': { ...ontimeEditable },
ontime: { ...ontimeEditable },
},
},
Input: {
@@ -50,6 +54,16 @@ const theme = extendTheme({
'ontime-filled': { ...ontimeInputFilled },
},
},
Modal: {
variants: {
ontime: { ...ontimeModal },
},
},
Tabs: {
variants: {
ontime: { ...ontimeTab },
},
},
Textarea: {
baseStyle: {
borderRadius: '3px',
@@ -60,16 +74,17 @@ const theme = extendTheme({
},
},
Tooltip: {
baseStyle: { ...ontimeTooltip},
baseStyle: { ...ontimeTooltip },
},
Switch: {
variants: {
'ontime': { ...ontimeSwitch },
ontime: { ...ontimeSwitch },
'ontime-on-light': { ...lightSwitch },
},
},
Select: {
variants: {
'ontime': { ...ontimeSelect },
ontime: { ...ontimeSelect },
},
},
Menu: {
+4 -4
View File
@@ -48,13 +48,13 @@ let tray = null;
try {
const ontimeServer = require(nodePath)
const { startServer, startOSCServer } = ontimeServer;
const { startDb, startServer, startOSCServer, startIntegrations } = ontimeServer;
await startDb();
// Start express server
loaded = await startServer();
// Start OSC Server
await startOSCServer();
await startIntegrations();
} catch (error) {
loaded = error;
}
+3 -2
View File
@@ -10,7 +10,7 @@
"body-parser": "^1.20.0",
"cors": "^2.8.5",
"dotenv": "^16.0.1",
"express": "^4.18.1",
"express": "^4.18.2",
"express-session": "^1.17.3",
"express-validator": "^6.14.2",
"lowdb": "^5.0.5",
@@ -24,8 +24,9 @@
"socket.io": "^4.5.4"
},
"devDependencies": {
"@types/express": "^4.17.15",
"@types/express": "^4.17.17",
"@types/node": "^16.11.7",
"@types/node-osc": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"esbuild": "^0.17.5",
+77 -50
View File
@@ -6,11 +6,11 @@ import cors from 'cors';
// import utils
import { join, resolve } from 'path';
import { config } from './config/config.js';
import { initiateOSC, shutdownOSCServer } from './controllers/OscController.js';
import { initSentry } from './modules/sentry.js';
import { currentDirectory, environment, isProduction, resolvedPath, uiPath } from './setup.js';
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
import { OSCSettings } from 'ontime-types';
// Import Routes
import { router as rundownRouter } from './routes/rundownRouter.js';
@@ -22,12 +22,9 @@ import { router as playbackRouter } from './routes/playbackRouter.js';
import { DataProvider } from './classes/data-provider/DataProvider.js';
import { socketProvider } from './classes/socket/SocketController.js';
import { eventTimer } from './services/TimerService.js';
import { promise } from './modules/loadDb.js';
import { TimerType } from 'ontime-types';
// TODO: apply code and remove, refs PR #290
const sharedType: TimerType = TimerType.CountDown;
console.log('WIP', sharedType);
import { dbLoadingProcess } from './modules/loadDb.js';
import { integrationService } from './services/integration-service/IntegrationService.js';
import { OscIntegration } from './services/integration-service/OscIntegration.js';
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
@@ -77,6 +74,7 @@ app.use((error, response) => {
});
/*************** START SERVICES ***************/
/* Override config
* ----------------
*
@@ -84,42 +82,35 @@ app.use((error, response) => {
* It can be overridden here by the settings in the db
* It can also be overridden on call
*
* Start order
* ----------------
*
* The services need to be started in a certain order,
* the enum below enforces that
*/
(async () => {
try {
await promise;
} catch (error) {
console.log(error);
enum OntimeStartOrder {
Error,
InitDB,
InitServer,
InitIO,
}
let step = OntimeStartOrder.InitDB;
const checkStart = (currentState) => {
if (step !== currentState) {
step = OntimeStartOrder.Error;
throw new Error('Init order error: startDb > startServer > startOsc > startIntegrations');
} else {
if (step === 1 || step === 2) {
step = step + 1;
}
}
})();
};
const { osc } = DataProvider.getData();
const oscIP = osc?.targetIP || config.osc.targetIP;
const oscOutPort = osc?.portOut || config.osc.portOut;
const oscInPort = osc?.port || config.osc.port;
const oscInEnabled = osc?.enabled !== undefined ? osc.enabled : config.osc.inputEnabled;
const serverPort = 4001; // hardcoded for now
/**
* @description starts OSC server
* @description starts OSC server
* @param overrideConfig
* @return {Promise<void>}
*/
export const startOSCServer = async (overrideConfig = null) => {
if (!oscInEnabled) {
socketServer.info('RX', 'OSC Input Disabled');
return;
}
// Setup default port
const oscSettings = {
port: overrideConfig?.port || oscInPort,
};
// Start OSC Server
socketServer.info('RX', `Starting OSC Server on port: ${oscInPort}`);
initiateOSC(oscSettings);
export const startDb = async () => {
checkStart(OntimeStartOrder.InitDB);
await dbLoadingProcess;
};
// create HTTP server
@@ -130,32 +121,67 @@ const expressServer = http.createServer(app);
* @return {Promise<string>}
*/
export const startServer = async () => {
// Start server
checkStart(OntimeStartOrder.InitServer);
const serverPort = 4001; // hardcoded for now
const returnMessage = `Ontime is listening on port ${serverPort}`;
expressServer.listen(serverPort, '0.0.0.0');
// init socket controller
await socketServer.initServer(expressServer);
socketServer.initServer(expressServer);
socketServer.info('SERVER', 'Socket initialised');
socketServer.info('SERVER', returnMessage);
socketServer.startListener();
return returnMessage;
};
/**
* starts integrations
* @description starts OSC server
* @description starts OSC server
* @param overrideConfig
* @return {Promise<void>}
*/
export const startIntegrations = async (overrideConfig = null) => {
const { http } = DataProvider.getData();
export const startOSCServer = async (overrideConfig = null) => {
checkStart(OntimeStartOrder.InitIO);
// OSC Config
const oscConfig = {
ip: oscIP,
port: overrideConfig?.port || oscOutPort,
const { osc } = DataProvider.getData();
if (!osc.enabledIn) {
socketServer.info('RX', 'OSC Input Disabled');
return;
}
// Setup default port
const oscSettings = {
...osc,
portIn: overrideConfig?.port || osc.portIn,
};
// Start OSC Server
socketServer.info('RX', `Starting OSC Server on port: ${oscSettings.portIn}`);
initiateOSC(oscSettings);
};
/**
* starts integrations
*/
export const startIntegrations = async (config?: { osc: OSCSettings }) => {
checkStart(OntimeStartOrder.InitIO);
const { osc } = config ?? DataProvider.getData();
if (!osc) {
return 'OSC Invalid configuration';
}
const oscIntegration = new OscIntegration();
const { success, message } = oscIntegration.init(osc);
socketServer.info('RX', message);
if (success) {
integrationService.register(oscIntegration);
}
};
/**
@@ -170,6 +196,7 @@ export const shutdown = async (exitCode = 0) => {
shutdownOSCServer();
eventTimer.shutdown();
socketServer.shutdown();
integrationService.shutdown();
process.exit(exitCode);
};
@@ -3,6 +3,7 @@
* and adds logic specific to ontime data
*/
import { data, db } from '../../modules/loadDb.js';
import { safeMerge } from './DataProvider.utils.js';
export class DataProvider {
static getData() {
@@ -39,6 +40,7 @@ export class DataProvider {
}
static async deleteEvent(eventId) {
// @ts-expect-error -- this will go away once we type db
data.rundown = Array.from(data.rundown).filter((e) => e.id !== eventId);
await this.persist();
}
@@ -49,6 +51,7 @@ export class DataProvider {
static async clearRundown() {
data.rundown = [];
// @ts-expect-error -- not sure how to type, this is library side
await db.write();
}
@@ -149,11 +152,12 @@ export class DataProvider {
}
static async persist() {
// @ts-expect-error -- not sure how to type, this is library side
await db.write();
}
static async mergeIntoData(newData) {
const mergedData = DataProvider.safeMerge(data, newData);
const mergedData = safeMerge(data, newData);
data.event = mergedData.event;
data.settings = mergedData.settings;
data.osc = mergedData.osc;
@@ -163,36 +167,4 @@ export class DataProvider {
data.rundown = mergedData.rundown;
await this.persist();
}
/**
* Merges two data objects
* @param {object} existing
* @param {object} newData
*/
static safeMerge(existing, newData) {
const mergedData = { ...existing };
if (typeof newData?.rundown !== 'undefined') {
mergedData.rundown = newData.rundown;
}
if (typeof newData?.event !== 'undefined') {
mergedData.event = { ...newData.event };
}
if (typeof newData?.settings !== 'undefined') {
mergedData.settings = { ...newData.settings };
}
if (typeof newData?.osc !== 'undefined') {
mergedData.osc = { ...newData.osc };
}
if (typeof newData?.http !== 'undefined') {
mergedData.http = { ...newData.http };
}
if (typeof newData?.aliases !== 'undefined') {
mergedData.aliases = [...newData.aliases];
}
if (typeof newData?.userFields !== 'undefined') {
mergedData.userFields = { ...existing.userFields, ...newData.userFields };
}
return mergedData;
}
}
@@ -0,0 +1,39 @@
/**
* Merges two data objects
* @param {object} existing
* @param {object} newData
*/
export function safeMerge(existing, newData) {
const { rundown, event, settings, osc, http, aliases, userFields } = newData || {};
return {
...existing,
rundown: rundown ?? existing.rundown,
event: { ...existing.event, ...event },
settings: { ...existing.settings, ...settings },
views: {
overrideStyles: false,
},
aliases: aliases ?? existing.aliases,
userFields: {
...existing.userFields,
...(userFields && Object.fromEntries(Object.entries(userFields).filter(([_, value]) => value !== null))),
},
osc: {
...existing.osc,
...osc,
subscriptions: {
...existing.osc?.subscriptions,
...(newData?.osc?.subscriptions || {}),
...(existing.osc?.subscriptions && newData?.osc?.subscriptions
? Object.keys(existing.osc.subscriptions).reduce((acc, key) => {
if (!(key in newData.osc.subscriptions)) {
acc[key] = existing.osc.subscriptions[key];
}
return acc;
}, {})
: {}),
},
},
http: { ...existing.http, ...http },
};
}
@@ -0,0 +1,302 @@
import { safeMerge } from '../DataProvider.utils.js';
describe('safeMerge', () => {
const existing = {
rundown: [],
event: {
title: 'existing title',
publicUrl: 'existing public URL',
backstageUrl: 'existing backstageUrl',
backstageInfo: 'existing backstageInfo',
endMessage: 'existing endMessage',
},
settings: {
app: 'ontime',
lock: true,
pinCode: null,
timeFormat: '24',
version: 2,
serverPort: 4001,
},
views: {
overrideStyles: false,
},
aliases: [],
userFields: {
user0: 'existing user0',
user1: 'existing user1',
},
osc: {
portIn: 8888,
portOut: 9999,
targetIP: '127.0.0.1',
enabledIn: false,
enabledOut: false,
subscriptions: {
onLoad: {
message: '',
enabled: false,
},
onStart: {
message: '',
enabled: false,
},
onPause: {
message: '',
enabled: false,
},
onStop: {
message: '',
enabled: false,
},
onUpdate: {
message: '',
enabled: false,
},
onFinish: {
message: '',
enabled: false,
},
},
},
http: {
enabled: true,
user: null,
pwd: null,
},
};
it('returns existing data if new data is not provided', () => {
const mergedData = safeMerge(existing, undefined);
expect(mergedData).toEqual(existing);
});
it('merges the rundown key', () => {
const newData = {
rundown: [{ name: 'item 1' }, { name: 'item 2' }],
};
const mergedData = safeMerge(existing, newData);
expect(mergedData.rundown).toEqual(newData.rundown);
});
it('merges the event key', () => {
const newData = {
event: {
title: 'new title',
publicInfo: 'new public info',
},
};
const mergedData = safeMerge(existing, newData);
expect(mergedData.event).toEqual({
title: 'new title',
publicUrl: 'existing public URL',
publicInfo: 'new public info',
backstageUrl: 'existing backstageUrl',
backstageInfo: 'existing backstageInfo',
endMessage: 'existing endMessage',
});
});
it('merges the settings key', () => {
const newData = {
settings: {
serverPort: 3000,
lock: '1234',
},
};
const mergedData = safeMerge(existing, newData);
expect(mergedData.settings).toEqual({
app: 'ontime',
version: 2,
serverPort: 3000,
lock: '1234',
pinCode: null,
timeFormat: '24',
});
});
it('merges the osc key', () => {
const newData = {
osc: {
portIn: 7777,
subscriptions: {
onStart: {
message: 'new message',
enabled: true,
},
},
},
};
const mergedData = safeMerge(existing, newData);
expect(mergedData.osc).toEqual({
portIn: 7777,
portOut: 9999,
targetIP: '127.0.0.1',
enabledIn: false,
enabledOut: false,
subscriptions: {
onLoad: {
message: '',
enabled: false,
},
onStart: {
message: 'new message',
enabled: true,
},
onPause: {
message: '',
enabled: false,
},
onStop: {
message: '',
enabled: false,
},
onUpdate: {
message: '',
enabled: false,
},
onFinish: {
message: '',
enabled: false,
},
},
});
});
it('should merge the aliases key when present', () => {
const existingData = {
rundown: [],
event: {
title: '',
publicUrl: '',
publicInfo: '',
backstageUrl: '',
backstageInfo: '',
endMessage: '',
},
settings: {
app: 'ontime',
version: 2,
serverPort: 4001,
lock: null,
pinCode: null,
timeFormat: '24',
},
views: {
overrideStyles: false,
},
aliases: [],
userFields: {
user0: 'user0',
user1: 'user1',
user2: 'user2',
user3: 'user3',
user4: 'user4',
user5: 'user5',
user6: 'user6',
user7: 'user7',
user8: 'user8',
user9: 'user9',
},
osc: {
portIn: 8888,
portOut: 9999,
targetIP: '127.0.0.1',
enabledIn: false,
enabledOut: false,
subscriptions: {
onLoad: {
message: '',
enabled: false,
},
onStart: {
message: '',
enabled: false,
},
onPause: {
message: '',
enabled: false,
},
onStop: {
message: '',
enabled: false,
},
onUpdate: {
message: '',
enabled: false,
},
onFinish: {
message: '',
enabled: false,
},
},
},
http: {
user: null,
pwd: null,
messages: {
onLoad: {
url: '',
enabled: false,
},
onStart: {
url: '',
enabled: false,
},
onUpdate: {
url: '',
enabled: false,
},
onPause: {
url: '',
enabled: false,
},
onStop: {
url: '',
enabled: false,
},
onFinish: {
url: '',
enabled: false,
},
},
enabled: true,
},
};
const newData = {
aliases: ['alias1', 'alias2'],
};
const mergedData = safeMerge(existingData, newData);
expect(mergedData.aliases).toEqual(newData.aliases);
});
it('merges userFields into existing object', () => {
const existing = {
userFields: {
user0: 'Alice',
user1: 'Bob',
},
};
const newData = {
userFields: {
user2: 'Charlie',
user3: 'David',
user4: null,
},
};
const expected = {
user0: 'Alice',
user1: 'Bob',
user2: 'Charlie',
user3: 'David',
};
const result = safeMerge(existing, newData);
expect(result.userFields).toEqual(expected);
});
});
@@ -1,4 +1,4 @@
import { DataProvider } from '../data-provider/DataProvider.js';
import { DataProvider } from '../data-provider/DataProvider.ts';
import { getRollTimers } from '../../services/rollUtils.js';
let instance;
@@ -1,53 +0,0 @@
import * as http from 'http';
/**
* @description Class contains logic towards outgoing HTTP communications
* @class
*/
export class HTTPIntegration {
constructor() {
// nothing to do here
}
/**
* @description Initializes oscClient
* @param {object} httpConfig - Http configurations options
*/
init(httpConfig) {}
/**
* @description Sends http get request from predefined messages
* @param {string} path - complete http path
*/
async send(path) {
if (path == null) {
console.log('HTTP ERROR: Message undefined');
return;
}
const options = new URL(path);
let str = '';
const req = http.request(options, (res) => {
console.log(`statusCode: ${res.statusCode}`);
res.on('data', function (chunk) {
str += chunk;
});
res.on('end', function () {
console.log(str);
});
});
req.on('error', (error) => {
console.error(error);
});
req.end();
}
shutdown() {
/* Nothing to shutdown */
}
}
-169
View File
@@ -1,169 +0,0 @@
import { Client, Message } from 'node-osc';
/**
* @description Class contains logic towards outgoing OSC communications
* @class
*/
export class OSCIntegration {
constructor() {
// OSC Client
this.ADDRESS = '/ontime';
this.oscClient = null;
}
/**
* @description Returns list of implemented messages
* @returns {object} implemented messages
*/
get implemented() {
return {
play: 'play',
pause: 'pause',
stop: 'stop',
previous: 'prev',
next: 'next',
reload: 'reload',
finished: 'finished',
time: 'time',
overtime: 'overtime',
title: 'title',
eventNumber: 'eventNumber',
presenter: 'presenter',
};
}
/**
* @description Initializes oscClient
* @param {object} oscConfig - oscClient configuration options
* @param {string} oscConfig.ip - oscClient object
* @param {number} oscConfig.port - OSC Destination Port
*/
init(oscConfig) {
const { ip, port } = oscConfig;
const validateType = typeof ip !== 'string' || typeof port !== 'number';
const validateNull = ip == null || port == null;
if (validateType || validateNull) {
return {
success: false,
message: `Config options incorrect`,
};
}
try {
this.oscClient = new Client(ip, port);
return {
success: true,
message: `Initialised OSC Client at ${ip}:${port}`,
};
} catch (error) {
this.oscClient = null;
return {
success: false,
message: `Failed initialising OSC Client: ${error}`,
};
}
}
/**
* @description Sends osc from predefined messages
* @param {string} messageType - message to be sent
* @param {string} [payload] - optional payload required in some message types
*/
async send(messageType, payload) {
const reply = {
success: true,
message: 'OSC Message sent',
};
if (this.oscClient == null) {
reply.success = false;
reply.message = 'Client not initialised';
return reply;
}
if (messageType == null) {
reply.success = false;
reply.message = 'Message undefined';
return reply;
}
// only specify special cases
switch (payload) {
case 'overtime': {
// Whether timer is negative
this.oscClient.send(`${this.ADDRESS}/overtime`, payload, (err) => {
if (err) {
reply.success = false;
reply.message = err;
}
});
break;
}
case 'title': {
if (payload != null && payload !== '') {
// Send Title of current event
this.oscClient.send(`${this.ADDRESS}/title`, payload, (err) => {
if (err) {
reply.success = false;
reply.message = err;
}
});
} else {
reply.success = false;
reply.message = 'Missing message data';
}
break;
}
case 'eventNumber': {
if (payload != null && payload !== '') {
// Send event number of current event
this.oscClient.send(`${this.ADDRESS}/eventNumber`, payload, (err) => {
if (err) {
reply.success = false;
reply.message = err;
}
});
} else {
reply.success = false;
reply.message = 'Missing message data';
}
break;
}
case 'presenter': {
if (payload != null && payload !== '') {
// Send timer data on current event
this.oscClient.send(`${this.ADDRESS}/presenter`, payload, (err) => {
if (err) {
reply.success = false;
reply.message = err;
}
});
} else {
reply.success = false;
reply.message = 'Missing message data';
}
break;
}
default: {
// catch all for messages, allows to add new messages
// but should be used with the integrations definition
const message = new Message(`${this.ADDRESS}/${messageType}`);
if (payload != null) message.append(payload);
this.oscClient.send(message, (err) => {
if (err) {
reply.success = false;
reply.message = err;
}
});
break;
}
}
return reply;
}
shutdown() {
// Shutdown client object
this.oscClient.close();
this.oscClient = null;
}
}
@@ -1,163 +0,0 @@
import { OSCIntegration } from '../Osc';
import { Server } from 'node-osc';
test('Class initialises correctly', () => {
const osc = new OSCIntegration();
expect(osc.ADDRESS).toBe('/ontime');
expect(osc.oscClient).toBe(null);
// defined objects
expect(osc.implemented.play).toBeDefined();
expect(osc.implemented.pause).toBeDefined();
expect(osc.implemented.stop).toBeDefined();
expect(osc.implemented.previous).toBeDefined();
expect(osc.implemented.next).toBeDefined();
expect(osc.implemented.reload).toBeDefined();
expect(osc.implemented.finished).toBeDefined();
expect(osc.implemented.time).toBeDefined();
expect(osc.implemented.overtime).toBeDefined();
expect(osc.implemented.title).toBeDefined();
expect(osc.implemented.eventNumber).toBeDefined();
expect(osc.implemented.presenter).toBeDefined();
// initialise client succeeds
const { ip, port } = { ip: '127.0.0.1', port: 12345 };
const init = osc.init({ ip, port });
expect(init.message).toBe(`Initialised OSC Client at ${ip}:${port}`);
expect(init.success).toBe(true);
expect(osc.oscClient).not.toBe(null);
// object shutdown as expected
osc.shutdown();
expect(osc.oscClient).toBe(null);
});
describe('OSC fails to initialise when incorrect data is given', () => {
it('IP of wrong type', () => {
const osc = new OSCIntegration();
const init = osc.init({ ip: 123, port: 8888 });
expect(init.message).toBe('Config options incorrect');
expect(init.success).toBe(false);
expect(osc.oscClient).toBe(null);
});
it('IP is null', () => {
const osc = new OSCIntegration();
const init = osc.init({ ip: null, port: 8888 });
expect(init.message).toBe('Config options incorrect');
expect(init.success).toBe(false);
expect(osc.oscClient).toBe(null);
});
it('Port of wrong type', () => {
const osc = new OSCIntegration();
const init = osc.init({ ip: 'localhost', port: 'test' });
expect(init.message).toBe('Config options incorrect');
expect(init.success).toBe(false);
expect(osc.oscClient).toBe(null);
});
it('Port is null', () => {
const osc = new OSCIntegration();
const init = osc.init({ ip: 'localhost', port: null });
expect(init.message).toBe('Config options incorrect');
expect(init.success).toBe(false);
expect(osc.oscClient).toBe(null);
});
});
test('Test messages sending', async () => {
const testPort = 9999;
const testIP = 'localhost';
const testPayload = 'test';
const osc = new OSCIntegration();
const messages = [];
// prepare dummy server to receive messages
const oscServer = new Server(testPort, testIP);
oscServer.on('message', (m) => {
messages.push({ yay: m });
});
// try and send a message before initialising
const test = await osc.send('test');
expect(test.success).toBe(false);
expect(test.message).toBe('Client not initialised');
// initialise osc
osc.init({ ip: testIP, port: testPort });
// try and send unrecognised message
const test2 = await osc.send('test');
expect(test2.success).toBe(true);
// send play message
const playAddress = osc.implemented.play;
const playSent = await osc.send(playAddress);
expect(playSent.success).toBe(true);
// send pause message
const pauseAddress = osc.implemented.pause;
const pauseSent = await osc.send(pauseAddress);
expect(pauseSent.success).toBe(true);
// send stop message
const stopAddress = osc.implemented.stop;
const stopSent = await osc.send(stopAddress);
expect(stopSent.success).toBe(true);
// send previous message
const previousAddress = osc.implemented.previous;
const previousSent = await osc.send(previousAddress);
expect(previousSent.success).toBe(true);
// send next message
const nextAddress = osc.implemented.next;
const nextSent = await osc.send(nextAddress);
expect(nextSent.success).toBe(true);
// send reload message
const reloadAddress = osc.implemented.reload;
const reloadSent = await osc.send(reloadAddress);
expect(reloadSent.success).toBe(true);
// send finished message
const finishedAddress = osc.implemented.finished;
const finishedSent = await osc.send(finishedAddress);
expect(finishedSent.success).toBe(true);
// send time message
const timeAddress = osc.implemented.time;
const timeSent = await osc.send(timeAddress);
expect(timeSent.success).toBe(true);
// send overtime message
const overtimeAddress = osc.implemented.overtime;
const overtimeSent = await osc.send(overtimeAddress, testPayload);
expect(overtimeSent.success).toBe(true);
// send title message
const titleAddress = osc.implemented.title;
const titleSent = await osc.send(titleAddress, testPayload);
expect(titleSent.success).toBe(true);
// send eventNumber message
const eventNumberAddress = osc.implemented.eventNumber;
const eventNumberSent = await osc.send(eventNumberAddress, testPayload);
expect(eventNumberSent.success).toBe(true);
// send timer message
const presenterAddress = osc.implemented.presenter;
const presenterSent = await osc.send(presenterAddress, testPayload);
expect(presenterSent.success).toBe(true);
// cleanup
await osc.shutdown();
await oscServer.close();
// see messagesObject
// expect(messages.length).toBe(5);
});
@@ -7,7 +7,7 @@ import { messageManager } from '../message-manager/MessageManager.js';
import { PlaybackService } from '../../services/PlaybackService.js';
import { ADDRESS_MESSAGE_CONTROL } from './socketConfig.js';
import { eventTimer, TimerService } from '../../services/TimerService.js';
import { eventTimer, TimerService } from '../../services/TimerService.ts';
import { EventLoader, eventLoader } from '../event-loader/EventLoader.js';
class SocketController {
-17
View File
@@ -1,25 +1,8 @@
export const config = {
timer: {
refresh: 1000,
},
server: {
port: 4001,
},
database: {
testdb: 'test-db',
directory: 'preloaded-db',
filename: 'db.json',
tablename: 'events',
},
osc: {
port: 8888,
portOut: 9999,
targetIP: '127.0.0.1',
inputEnabled: true,
},
http: {
user: '',
pwd: '',
enabled: true,
},
};
@@ -1,4 +1,6 @@
import { Server } from 'node-osc';
import { OSCSettings } from 'ontime-types';
import { PlaybackService } from '../services/PlaybackService.js';
import { messageManager } from '../classes/message-manager/MessageManager.js';
import { socketProvider } from '../classes/socket/SocketController.js';
@@ -14,11 +16,10 @@ export const shutdownOSCServer = () => {
};
/**
* @description initialises OSC server
* @param {object} config
* Initialises OSC server
*/
export const initiateOSC = (config) => {
oscServer = new Server(config.port, '0.0.0.0');
export const initiateOSC = (config: OSCSettings) => {
oscServer = new Server(config.portIn, '0.0.0.0');
oscServer.on('error', console.error);
@@ -34,7 +35,7 @@ export const initiateOSC = (config) => {
// get first part before (ontime)
if (address !== 'ontime') {
console.error('RX', `OSC IN: Message address ${address} not recognised`);
console.error('RX', `OSC IN: Message address ${address} not recognised`, msg);
return;
}
@@ -123,10 +124,7 @@ export const initiateOSC = (config) => {
try {
const eventIndex = Number(args);
if (isNaN(eventIndex) || eventIndex <= 0) {
socketProvider.error(
'RX',
`OSC IN: event index not recognised or out of range ${eventIndex}`
);
socketProvider.error('RX', `OSC IN: event index not recognised or out of range ${eventIndex}`);
} else {
PlaybackService.loadByIndex(eventIndex - 1);
}
@@ -1,6 +1,6 @@
import { removeUndefined } from '../utils/parserUtils.js';
import { failEmptyObjects } from '../utils/routerUtils.js';
import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { DataProvider } from '../classes/data-provider/DataProvider.ts';
// Create controller for GET request to 'event'
export const getEvent = async (req, res) => {
@@ -2,7 +2,7 @@ import fs from 'fs';
import { networkInterfaces } from 'os';
import { generateId } from 'ontime-utils';
import { fileHandler } from '../utils/parser.js';
import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { DataProvider } from '../classes/data-provider/DataProvider.ts';
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
import { mergeObject } from '../utils/parserUtils.js';
import { PlaybackService } from '../services/PlaybackService.js';
@@ -65,10 +65,11 @@ export const validateSettings = [
* @description Validates object for POST /ontime/osc
*/
export const validateOSC = [
body('port').exists().isInt({ min: 0, max: 65353 }),
body('portOut').exists().isInt({ min: 0, max: 65353 }),
body('portIn').exists().isInt({ min: 1024, max: 65535 }),
body('portOut').exists().isInt({ min: 1024, max: 65535 }),
body('targetIP').exists().isIP(),
body('enabled').exists().isBoolean(),
body('enabledIn').exists().isBoolean(),
body('enabledOut').exists().isBoolean(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
@@ -1,4 +1,4 @@
import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { DataProvider } from '../classes/data-provider/DataProvider.ts';
import { failEmptyObjects } from '../utils/routerUtils.js';
import {
addEvent,
+3 -5
View File
@@ -1,16 +1,14 @@
// import { promise } from './modules/loadDb.js';
import { startOSCServer, startServer } from './app.js';
import { startDb, startIntegrations, startOSCServer, startServer } from './app.js';
async function startOntime() {
try {
// await promise;
await startDb();
// Start express server
const loaded = await startServer();
console.log(loaded);
// Start OSC Server (API)
await startOSCServer();
await startIntegrations();
} catch (error) {
console.log('Error starting Ontime');
console.log(error);
@@ -1,4 +1,6 @@
export const dbModel = {
import { DatabaseModel } from 'ontime-types';
export const dbModel: DatabaseModel = {
rundown: [],
event: {
title: '',
@@ -33,10 +35,37 @@ export const dbModel = {
user9: 'user9',
},
osc: {
port: 8888,
portIn: 8888,
portOut: 9999,
targetIP: '127.0.0.1',
enabled: true,
enabledIn: false,
enabledOut: false,
subscriptions: {
onLoad: {
message: '',
enabled: false,
},
onStart: {
message: '',
enabled: false,
},
onPause: {
message: '',
enabled: false,
},
onStop: {
message: '',
enabled: false,
},
onUpdate: {
message: '',
enabled: false,
},
onFinish: {
message: '',
enabled: false,
},
},
},
http: {
user: null,
@@ -1,6 +1,8 @@
import { Low } from 'lowdb';
import { JSONFile } from 'lowdb/node';
import { copyFileSync, existsSync } from 'fs';
import { DatabaseModel } from 'ontime-types';
import { ensureDirectory } from '../utils/fileManagement.js';
import { validateFile } from '../utils/parserUtils.js';
import { dbModel } from '../models/dataModel.js';
@@ -51,7 +53,7 @@ const parseDb = async (fileToRead, adapterToUse) => {
async function loadDb() {
const dbInDisk = populateDb();
const adapter = new JSONFile(dbInDisk);
const adapter = new JSONFile<DatabaseModel>(dbInDisk);
const db = new Low(adapter);
const data = await parseDb(dbInDisk, db);
@@ -63,11 +65,11 @@ async function loadDb() {
}
export let db = {};
export let data = {};
export const promise = loadDb();
export let data = {} as DatabaseModel;
export const dbLoadingProcess = loadDb();
const init = async () => {
const dbProvider = await promise;
const dbProvider = await dbLoadingProcess;
db = dbProvider.db;
data = dbProvider.data;
};
+1 -1
View File
@@ -3,7 +3,7 @@
*/
import { socketProvider } from '../classes/socket/SocketController.js';
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
import { eventTimer, TimerService } from './TimerService.js';
import { eventTimer, TimerService } from './TimerService.ts';
/**
* Service manages playback status of app
+2 -2
View File
@@ -1,5 +1,5 @@
import { generateId } from 'ontime-utils';
import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { DataProvider } from '../classes/data-provider/DataProvider.ts';
import {
block as blockDef,
delay as delayDef,
@@ -7,7 +7,7 @@ import {
} from '../models/eventsDefinition.js';
import { MAX_EVENTS } from '../settings.js';
import { EventLoader, eventLoader } from '../classes/event-loader/EventLoader.js';
import { eventTimer } from './TimerService.js';
import { eventTimer } from './TimerService.ts';
import { socketProvider } from '../classes/socket/SocketController.js';
/**
@@ -1,15 +1,40 @@
import { TimerLifeCycle } from 'ontime-types';
import { runtimeState } from '../stores/EventStore.js';
import { PlaybackService } from './PlaybackService.js';
import { updateRoll } from './rollUtils.js';
import { DAY_TO_MS } from '../utils/time.js';
import { integrationService } from './integration-service/IntegrationService.js';
export class TimerService {
private readonly _interval: NodeJS.Timer;
private playback: string;
private loadedTimerId: null;
private _pausedInterval: number;
private _pausedAt: number | null;
private _secondaryTarget: number | null;
timer: {
clock: number;
current: number | null;
elapsed: number | null;
expectedFinish: number | null;
addedTime: number;
startedAt: number | null;
finishedAt: number | null;
secondaryTimer: number | null;
selectedEventId: string | null;
duration: number | null;
};
/**
* @constructor
* @param {object} [timerConfig]
* @param {number} [timerConfig.refresh]
*/
constructor(timerConfig) {
constructor(timerConfig?) {
this._clear();
this._interval = setInterval(() => this.update(), timerConfig?.refresh || 1000);
}
@@ -45,7 +70,7 @@ export class TimerService {
return Math.max(
this.timer.startedAt + this.timer.duration + this._pausedInterval + this.timer.addedTime,
this.timer.startedAt
this.timer.startedAt,
);
}
@@ -64,6 +89,8 @@ export class TimerService {
startedAt: null,
finishedAt: null,
secondaryTimer: null,
selectedEventId: null,
duration: null,
};
this.loadedTimerId = null;
this._pausedInterval = 0;
@@ -138,6 +165,7 @@ export class TimerService {
_onLoad() {
runtimeState.set('playback', this.playback);
runtimeState.set('ontime-timer', this.timer);
integrationService.dispatch(TimerLifeCycle.onLoad);
}
start() {
@@ -172,6 +200,7 @@ export class TimerService {
_onStart() {
runtimeState.set('playback', this.playback);
runtimeState.set('ontime-timer', this.timer);
integrationService.dispatch(TimerLifeCycle.onStart);
}
pause() {
@@ -188,6 +217,7 @@ export class TimerService {
_onPause() {
runtimeState.set('playback', this.playback);
runtimeState.set('ontime-timer', this.timer);
integrationService.dispatch(TimerLifeCycle.onPause);
}
stop() {
@@ -202,6 +232,7 @@ export class TimerService {
_onStop() {
runtimeState.set('playback', this.playback);
runtimeState.set('ontime-timer', this.timer);
integrationService.dispatch(TimerLifeCycle.onStop);
}
/**
@@ -249,8 +280,7 @@ export class TimerService {
secondaryTimer: this.timer.secondaryTimer,
_secondaryTarget: this._secondaryTarget,
};
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } =
updateRoll(tempCurrentTimer);
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updateRoll(tempCurrentTimer);
this.timer.current = updatedTimer;
this.timer.secondaryTimer = updatedSecondaryTimer;
@@ -272,11 +302,7 @@ export class TimerService {
}
this.timer.current =
this.timer.startedAt +
this.timer.duration +
this.timer.addedTime +
this._pausedInterval -
this.timer.clock;
this.timer.startedAt + this.timer.duration + this.timer.addedTime + this._pausedInterval - this.timer.clock;
this.timer.elapsed = this.timer.duration - this.timer.current;
if (this.playback === 'play' && this.timer.current <= 0 && this.timer.finishedAt === null) {
@@ -294,11 +320,13 @@ export class TimerService {
_onUpdate() {
runtimeState.set('playback', this.playback);
runtimeState.set('ontime-timer', this.timer);
integrationService.dispatch(TimerLifeCycle.onUpdate);
}
_onFinish() {
runtimeState.set('playback', this.playback);
runtimeState.set('ontime-timer', this.timer);
integrationService.dispatch(TimerLifeCycle.onFinish);
}
roll(currentEvent, nextEvent, timers) {
@@ -0,0 +1,24 @@
import { TimerLifeCycle, OscSubscription } from 'ontime-types';
export type TimerLifeCycleKey = keyof typeof TimerLifeCycle;
export default interface IIntegration {
subscriptions: OscSubscription;
init: (config: unknown) => OperationReturn;
dispatch: (action: TimerLifeCycleKey, state?: object) => OperationReturn;
emit: (...args: unknown[]) => unknown;
shutdown: () => void;
}
// either went well, or explain what failed
type OperationReturn = ReturnOnSuccess | ReturnOnError;
type ReturnOnSuccess = {
success: true;
message?: string;
};
type ReturnOnError = {
success: false;
message: string;
};
@@ -0,0 +1,34 @@
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
import { runtimeState } from '../../stores/EventStore.js';
class IntegrationService {
private integrations: IIntegration[];
constructor() {
this.integrations = [];
}
register(integrationService: IIntegration) {
this.integrations.push(integrationService);
}
unregister(integrationService: IIntegration) {
this.integrations = this.integrations.filter((int) => int !== integrationService);
}
dispatch(action: TimerLifeCycleKey) {
const state = runtimeState.poll();
this.integrations.forEach((integration) => {
integration.dispatch(action, state);
});
}
shutdown() {
console.log('Shutdown integrations');
this.integrations.forEach((integration) => {
integration.shutdown();
});
}
}
export const integrationService = new IntegrationService();
@@ -0,0 +1,118 @@
import { ArgumentType, Client, Message } from 'node-osc';
import { OSCSettings, OscSubscription } from 'ontime-types';
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
import { parseTemplate } from './integrationUtils.js';
import { isObject } from '../../utils/varUtils.js';
type Action = TimerLifeCycleKey | string;
/**
* @description Class contains logic towards outgoing OSC communications
* @class
*/
export class OscIntegration implements IIntegration {
protected oscClient: null | Client;
subscriptions: OscSubscription;
constructor() {
this.oscClient = null;
this.subscriptions = {};
}
/**
* Initializes oscClient
*/
init(config: OSCSettings) {
const { targetIP, portOut, subscriptions } = config;
this.initSubscriptions(subscriptions);
// runtime validation
const validateType = typeof targetIP !== 'string' || typeof portOut !== 'number';
const validateNull = !targetIP || !portOut;
if (validateType || validateNull) {
return {
success: false,
message: `Config options incorrect`,
};
}
try {
this.oscClient = new Client(targetIP, portOut);
return {
success: true,
message: `OSC integration client connected to ${targetIP}:${portOut}`,
};
} catch (error) {
this.oscClient = null;
return {
success: false,
message: `Failed initialising OSC Client: ${error}`,
};
}
}
initSubscriptions(subscriptionOptions: OscSubscription) {
this.subscriptions = { ...this.subscriptions, ...subscriptionOptions };
}
dispatch(action: Action, state?: object) {
if (!this.oscClient) {
return {
success: false,
message: 'Client not initialised',
};
}
if (!action) {
return {
success: false,
message: 'OSC called with no action',
};
}
// check subscriptions for action
const { enabled, message } = this.subscriptions?.[action] || {};
if (enabled) {
const parsedMessage = parseTemplate(message, state || {});
this.emit('address/', parsedMessage);
}
}
emit(path: string, payload?: ArgumentType) {
const message = new Message(path);
if (payload) {
try {
if (isObject(payload)) {
message.append(JSON.stringify(payload));
} else {
message.append(payload);
}
} catch (error) {
console.log('OSC ERROR', error, payload);
}
}
this.oscClient.send(message, (error) => {
if (error) {
return {
success: false,
message: `error is here ${JSON.stringify(error)}`,
};
}
return {
success: true,
message: 'OSC Message sent',
};
});
}
shutdown() {
console.log('Shutting down OSC integration');
if (this.oscClient) {
this.oscClient?.close();
this.oscClient = null;
}
}
}
@@ -0,0 +1,53 @@
import { parseTemplate } from './integrationUtils.js';
describe('integrationUtils', () => {
it('correctly parses a given string', () => {
const mockState = { test: 'this' };
const testString = 'That should replace {{test}}';
const expected = `That should replace ${mockState.test}`;
const result = parseTemplate(testString, mockState);
expect(result).toStrictEqual(expected);
});
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 = parseTemplate(testString, mockState);
expect(result).toStrictEqual(expected);
});
it('correctly parses a string without templates', () => {
const testString = 'That should replace {test}';
const result = parseTemplate(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 = parseTemplate(testString, mockState);
expect(result).toStrictEqual(expected);
});
it('doesnt yet handle nested variables', () => {
const mockState = {
timer: {
time: '10',
},
enabled: 'is',
};
const testString = 'Timer {{enabled}} enabled with {{timer.time}}ms interval';
const expected = 'Timer is enabled with {{timer.time}}ms interval';
const result = parseTemplate(testString, mockState);
expect(result).toStrictEqual(expected);
});
});
@@ -0,0 +1,18 @@
// any value inside double curly braces {{val}}
const placeholderRegex = /{{(.*?)}}/g;
/**
* Parses a templated string
*/
export function parseTemplate(template: string, state: object): string {
let parsedTemplate = template;
let match;
while ((match = placeholderRegex.exec(template)) !== null) {
const variableName = match[1];
if (Object.hasOwn(state, variableName)) {
parsedTemplate = parsedTemplate.replace(match[0], state[variableName]);
}
}
return parsedTemplate;
}
@@ -0,0 +1,11 @@
import { isObject } from '../varUtils.js';
describe('isObject', () => {
const testCases = [1, 0, false, undefined, 'test', null, () => undefined, []];
testCases.forEach((test) => {
it(`recognises normal primitives ${test}`, () => {
const result = isObject(test);
expect(result).toBe(false);
});
});
});
@@ -1,4 +1,6 @@
import { generateId } from 'ontime-utils';
import { OSCSettings } from 'ontime-types';
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
import { dbModel } from '../models/dataModel.js';
import { validateEvent } from './parser.js';
@@ -148,31 +150,27 @@ export const parseViews = (data, enforce) => {
/**
* Parse osc portion of an entry
* @param {object} data - data object
* @param {boolean} enforce - whether to create a definition if one is missing
* @returns {object} - event object data
*/
export const parseOsc = (data, enforce) => {
let newOsc = {};
export const parseOsc = (
data: { osc?: Partial<OSCSettings> },
enforce: boolean,
): OSCSettings | Record<string, never> => {
if ('osc' in data) {
console.log('Found OSC definition, importing...');
const s = data.osc;
const osc = {};
if (s.port) osc.port = s.port;
if (s.portOut) osc.portOut = s.portOut;
if (s.targetIP) osc.targetIP = s.targetIP;
if (typeof s.enabled !== 'undefined') osc.enabled = s.enabled;
// write to db
newOsc = {
...dbModel.osc,
...osc,
const loadedConfig = data?.osc || {};
return {
portIn: loadedConfig.portIn ?? dbModel.osc.portIn,
portOut: loadedConfig.portOut ?? dbModel.osc.portOut,
targetIP: loadedConfig.targetIP ?? dbModel.osc.targetIP,
enabledIn: loadedConfig.enabledIn ?? dbModel.osc.enabledIn,
enabledOut: loadedConfig.enabledOut ?? dbModel.osc.enabledOut,
subscriptions: loadedConfig.subscriptions ?? dbModel.osc.subscriptions,
};
} else if (enforce) {
newOsc = { ...dbModel.osc };
console.log(`Created OSC object in db`);
}
return newOsc;
return { ...dbModel.osc };
} else return {};
};
/**
@@ -185,18 +183,21 @@ export const parseHttp = (data, enforce) => {
const newHttp = {};
if ('http' in data) {
console.log('Found HTTP definition, importing...');
const h = data.osc;
const h = data.http;
const http = {};
// @ts-expect-error -- not yet
if (h.user) http.user = h.user;
// @ts-expect-error -- not yet
if (h.pwd) http.pwd = h.pwd;
// write to db
// @ts-expect-error -- not yet
newHttp.http = {
...dbModel.http,
...http,
};
} else if (enforce) {
// @ts-expect-error -- not yet
newHttp.http = { ...dbModel.http };
console.log(`Created http object in db`);
}
+3
View File
@@ -0,0 +1,3 @@
export function isObject(variable: unknown): boolean {
return typeof variable === 'object' && variable !== null && !Array.isArray(variable);
}
@@ -0,0 +1,12 @@
import { OSCSettings } from './core/OscSettings.type.js';
export type DatabaseModel = {
rundown: any;
event: any;
settings: any;
views: any;
aliases: any;
userFields: any;
osc: OSCSettings;
http: any;
};
@@ -1,7 +1,7 @@
enum TimerType {
enum TimerTypeType {
CountDown = 'count-down',
CountUp = 'count-up',
Clock = 'clock'
}
export default TimerType
export default TimerTypeType
@@ -0,0 +1,13 @@
import { TimerLifeCycle } from './TimerLifecycle.type';
export type TimerLifeCycleKey = keyof typeof TimerLifeCycle;
export type OscSubscription = { [key in TimerLifeCycleKey]?: { message: string; enabled: boolean } };
export interface OSCSettings {
portIn: number;
portOut: number;
targetIP: string;
enabledIn: boolean;
enabledOut: boolean;
subscriptions: OscSubscription;
}
@@ -0,0 +1,8 @@
export enum TimerLifeCycle {
onLoad = 'onLoad',
onStart = 'onStart',
onPause = 'onPause',
onStop = 'onStop',
onUpdate = 'onUpdate',
onFinish = 'onFinish',
}
+23 -2
View File
@@ -1,3 +1,24 @@
import TimerType from './definitions/TimerType.js';
import TimerTypeType from './definitions/TimerType.type.js';
import { DatabaseModel } from './definitions/DataModel.type.js';
import { TimerLifeCycle } from './definitions/core/TimerLifecycle.type.js';
import { OSCSettings, OscSubscription } from './definitions/core/OscSettings.type.js';
export { TimerType };
// DATA MODEL
export type { DatabaseModel };
// ---> Rundown
export type { TimerTypeType };
// ---> Event
// ---> Settings
// ---> Views
// ---> Aliases
// ---> User Fields
// ---> OSC
export type { OscSubscription, OSCSettings };
// ---> HTTP
// SERVER
export { TimerLifeCycle };
// CLIENT
+25 -8
View File
@@ -79,6 +79,7 @@ importers:
react-beautiful-dnd: ^13.1.1
react-dom: ^18.2.0
react-fast-compare: ^3.2.0
react-hook-form: ^7.43.1
react-qr-code: ^2.0.11
react-router-dom: ^6.3.0
react-table: ^7.7.0
@@ -116,6 +117,7 @@ importers:
react-beautiful-dnd: 13.1.1_biqbaboplfbrettd7655fr4n2y
react-dom: 18.2.0_react@18.2.0
react-fast-compare: 3.2.0
react-hook-form: 7.43.1_react@18.2.0
react-qr-code: 2.0.11_react@18.2.0
react-router-dom: 6.6.2_biqbaboplfbrettd7655fr4n2y
react-table: 7.8.0_react@18.2.0
@@ -177,8 +179,9 @@ importers:
specifiers:
'@sentry/node': ^7.24.1
'@sentry/tracing': ^7.24.1
'@types/express': ^4.17.15
'@types/express': ^4.17.17
'@types/node': ^16.11.7
'@types/node-osc': ^6.0.0
'@typescript-eslint/eslint-plugin': ^5.48.1
'@typescript-eslint/parser': ^5.48.1
body-parser: ^1.20.0
@@ -187,7 +190,7 @@ importers:
esbuild: ^0.17.5
eslint: ^8.31.0
eslint-plugin-prettier: ^4.2.1
express: ^4.18.1
express: ^4.18.2
express-session: ^1.17.3
express-validator: ^6.14.2
lowdb: ^5.0.5
@@ -225,8 +228,9 @@ importers:
passport-local: 1.0.0
socket.io: 4.5.4
devDependencies:
'@types/express': 4.17.15
'@types/express': 4.17.17
'@types/node': 16.18.11
'@types/node-osc': 6.0.0
'@typescript-eslint/eslint-plugin': 5.48.1_3jon24igvnqaqexgwtxk6nkpse
'@typescript-eslint/parser': 5.48.1_iukboom6ndih5an6iafl45j2fe
esbuild: 0.17.5
@@ -2922,19 +2926,19 @@ packages:
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
dev: true
/@types/express-serve-static-core/4.17.32:
resolution: {integrity: sha512-aI5h/VOkxOF2Z1saPy0Zsxs5avets/iaiAJYznQFm5By/pamU31xWKL//epiF4OfUA2qTOc9PV6tCUjhO8wlZA==}
/@types/express-serve-static-core/4.17.33:
resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==}
dependencies:
'@types/node': 18.11.18
'@types/qs': 6.9.7
'@types/range-parser': 1.2.4
dev: true
/@types/express/4.17.15:
resolution: {integrity: sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ==}
/@types/express/4.17.17:
resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==}
dependencies:
'@types/body-parser': 1.19.2
'@types/express-serve-static-core': 4.17.32
'@types/express-serve-static-core': 4.17.33
'@types/qs': 6.9.7
'@types/serve-static': 1.15.0
dev: true
@@ -3021,6 +3025,10 @@ packages:
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
dev: true
/@types/node-osc/6.0.0:
resolution: {integrity: sha512-25DwJOFe1KueUZz2oIURT3qCMQ28Jdvy9JqGz8d0mKM1Mlx0agHD9N3S0hMKajVCjw7TGtf3gGjbl5gDCFfIWQ==}
dev: true
/@types/node/16.18.11:
resolution: {integrity: sha512-3oJbGBUWuS6ahSnEq1eN2XrCyf4YsWI8OyCvo7c64zQJNplk3mO84t53o8lfTk+2ji59g5ycfc6qQ3fdHliHuA==}
dev: true
@@ -7462,6 +7470,15 @@ packages:
use-sidecar: 1.1.2_kzbn2opkn2327fwg5yzwzya5o4
dev: false
/react-hook-form/7.43.1_react@18.2.0:
resolution: {integrity: sha512-+s3+s8LLytRMriwwuSqeLStVjRXFGxgjjx2jED7Z+wz1J/88vpxieRQGvJVvzrzVxshZ0BRuocFERb779m2kNg==}
engines: {node: '>=12.22.0'}
peerDependencies:
react: ^16.8.0 || ^17 || ^18
dependencies:
react: 18.2.0
dev: false
/react-is/16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}