mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-17 13:23:35 +00:00
V2 integration (#293)
* feat: integrations and lifecycles * refactor: prevent issues with start order
This commit is contained in:
@@ -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,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export const isOnlyNumbers = /^\d+$/;
|
||||
export const isIPAddress = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+11
-22
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+8
-9
@@ -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,6 +7,13 @@
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
// reset box-sizing
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body,
|
||||
html,
|
||||
.App {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
};
|
||||
@@ -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: {},
|
||||
};
|
||||
|
||||
@@ -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
|
||||
},
|
||||
};
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user