mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-07 08:23:55 +00:00
Feat/navigate (#81)
* feat/navigate upgrade react router * feat/navigate migrate to react router 6 * feat/navigate style modal * feat/navigate create endpoints for app settings * feat/navigate protect editor with pin * feat/navigate apply dynamic routing draft * feat/navigate upgrade relevant packages * feat/navigate restructure directory and add tests * feat/navigate test aliases validation * feat/navigate validate aliases before sending * feat/navigate create data endpoint * feat/navigate config: prettier * feat/navigate invalidate empty strings * feat/navigate create endpoints * feat/navigate parse on import * feat/navigate navigate to alias * feat/navigate user help and sample data * feat/navigate refact aliases modal * feat/navigate refact settings style * feat/navigate update sample db * feat/navigate link is relative to hostname * feat/navigate navigate to first match * feat/navigate update readme and version bump * feat/navigate config: create shared module * feat/navigate config: cheat module install * feat/navigate fix tests * feat/navigate run tests in pull request * Update ontime_cy.yml
This commit is contained in:
@@ -1,174 +1,321 @@
|
||||
import { Button, IconButton } from '@chakra-ui/button';
|
||||
import { FiPlus, FiMinus } from 'react-icons/fi';
|
||||
import { FiInfo, FiMinus, FiSun } from 'react-icons/fi';
|
||||
import { ModalBody } from '@chakra-ui/modal';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
import { fetchEvent } from 'app/api/eventApi';
|
||||
import { useState } from 'react';
|
||||
import { getAliases, postAliases } from '../../app/api/ontimeApi';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useFetch } from 'app/hooks/useFetch';
|
||||
import { EVENT_TABLE } from 'app/api/apiConstants';
|
||||
import { ALIASES } from 'app/api/apiConstants';
|
||||
import style from './Modals.module.scss';
|
||||
import { viewerLinks } from '../../app/appConstants';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
import { validateAlias } from '../../app/utils/aliases';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
import handleLink from '../../common/utils/handleLink';
|
||||
|
||||
export default function AliasesModal() {
|
||||
const { data, status, isError } = useFetch(EVENT_TABLE, fetchEvent);
|
||||
const { data, status, refetch } = useFetch(ALIASES, getAliases);
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [aliases, setAliases] = useState([]);
|
||||
const host = window.location.host;
|
||||
|
||||
/**
|
||||
* Set formdata from server state
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (data == null) return;
|
||||
if (changed) return;
|
||||
setAliases([...data]);
|
||||
}, [changed, data]);
|
||||
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
// NOTHING HERE YET
|
||||
setSubmitting(true);
|
||||
|
||||
const validatedAliases = [...aliases];
|
||||
let errors = false;
|
||||
for (const alias of validatedAliases) {
|
||||
// validate url
|
||||
const isURLValid = validateAlias(alias.pathAndParams);
|
||||
if (!isURLValid.status) {
|
||||
alias.urlError = isURLValid.message;
|
||||
errors = true;
|
||||
} else {
|
||||
alias.urlError = undefined;
|
||||
}
|
||||
// validate alias
|
||||
const isAliasValid = validateAlias(alias.alias);
|
||||
if (!isAliasValid.status) {
|
||||
alias.aliasError = isAliasValid.message;
|
||||
errors = true;
|
||||
} else {
|
||||
alias.aliasError = undefined;
|
||||
}
|
||||
}
|
||||
setAliases(validatedAliases);
|
||||
|
||||
if (!errors) {
|
||||
await postAliases(aliases);
|
||||
await refetch();
|
||||
setChanged(false);
|
||||
}
|
||||
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
// Hardcoded links for now
|
||||
// it will need dynamic PORT assignment
|
||||
const speakerLink = 'http://localhost:4001/speaker';
|
||||
const smLink = 'http://localhost:4001/sm';
|
||||
const publicLink = 'http://localhost:4001/public';
|
||||
const pipLink = 'http://localhost:4001/pip';
|
||||
const studioLink = 'http://localhost:4001/studio';
|
||||
/**
|
||||
* Creates a new alias in state with a temporary id
|
||||
*/
|
||||
const addNew = () => {
|
||||
if (aliases.length > 20) {
|
||||
emitError('Maximum amount of aliases reacted (20)');
|
||||
return;
|
||||
}
|
||||
|
||||
const emptyAlias = {
|
||||
id: Math.floor(Math.random() * 1000),
|
||||
enabled: false,
|
||||
alias: '',
|
||||
pathAndParams: '',
|
||||
};
|
||||
setAliases((prevState) => [...prevState, emptyAlias]);
|
||||
setChanged(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an alias by a given id
|
||||
* @param {string} id - id of alias to delete
|
||||
*/
|
||||
const deleteAlias = (id) => {
|
||||
setAliases((prevState) => [...prevState.filter((a) => a.id !== id)]);
|
||||
setChanged(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets enabled flag to true / false
|
||||
* @param {string} id - object id
|
||||
* @param {boolean} isEnabled - whether to enable / disable flag
|
||||
*/
|
||||
const setEnabled = (id, isEnabled) => {
|
||||
const aliasesState = [...aliases];
|
||||
for (const a of aliasesState) {
|
||||
if (a.id === id) {
|
||||
if (isEnabled) {
|
||||
if (a.alias === '' || a.pathAndParams === '') {
|
||||
emitError('Alias incomplete');
|
||||
break;
|
||||
}
|
||||
|
||||
const isRepeated = aliases.some(
|
||||
(r) => a.alias === r.alias && r.enabled
|
||||
);
|
||||
if (isRepeated) {
|
||||
emitError('There is already an alias with this name');
|
||||
break;
|
||||
}
|
||||
}
|
||||
a.enabled = isEnabled;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setChanged(true);
|
||||
setAliases(aliasesState);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
*/
|
||||
const revert = async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles change of input field in local state
|
||||
* @param {number} index - index of item in array
|
||||
* @param {string} field - object parameter to update
|
||||
* @param {string} value - new object parameter value
|
||||
*/
|
||||
const handleChange = (index, field, value) => {
|
||||
const temp = [...aliases];
|
||||
temp[index][field] = value;
|
||||
setAliases(temp);
|
||||
setChanged(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalBody className={style.modalBody}>
|
||||
<p className={style.notes}>
|
||||
Configure easy to use URL Aliases
|
||||
<br />
|
||||
🔥 Changes take effect on save 🔥
|
||||
</p>
|
||||
<form onSubmit={submitHandler}>
|
||||
<ModalBody className={style.modalBody}>
|
||||
<p className={style.notes}>
|
||||
Configure easy to use URL Aliases
|
||||
<div className={style.modalFields}>
|
||||
<div className={style.hSeparator}>Default URLs</div>
|
||||
<div className={style.blockNotes}>
|
||||
{viewerLinks.map((l) => (
|
||||
<a
|
||||
href={l.link}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className={style.flexNote}
|
||||
key={l.link}
|
||||
onClick={() => handleLink(`${host}/${l.link}`)}
|
||||
>
|
||||
{`${l.label} - ${l.link}`}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className={style.hSeparator}>Custom Aliases</div>
|
||||
<div className={style.blockNotes}>
|
||||
<span className={style.inlineFlex}>
|
||||
<FiInfo color='#2b6cb0' fontSize={'2em'} />
|
||||
URL aliases are useful in two main scenarios
|
||||
</span>
|
||||
<span className={style.labelNote}>Complicated URLs</span>
|
||||
<br />
|
||||
❄️ Feature is not yet implemented ❄️
|
||||
</p>
|
||||
|
||||
<span>Default URLs</span>
|
||||
|
||||
<div className={style.highNotes}>
|
||||
<p className={style.flexNote}>
|
||||
Presenter Screen <br />
|
||||
<a
|
||||
href={speakerLink}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className={style.label}
|
||||
>
|
||||
{speakerLink}
|
||||
</a>
|
||||
</p>
|
||||
<p className={style.flexNote}>
|
||||
Backstage / Stage Manager Screen <br />
|
||||
<a
|
||||
href={smLink}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className={style.label}
|
||||
>
|
||||
{smLink}
|
||||
</a>
|
||||
</p>
|
||||
<p className={style.flexNote}>
|
||||
Public / Foyer Screen <br />
|
||||
<a
|
||||
href={publicLink}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className={style.label}
|
||||
>
|
||||
{publicLink}
|
||||
</a>
|
||||
</p>
|
||||
<p className={style.flexNote}>
|
||||
Picture in Picture Screen <br />
|
||||
<a
|
||||
href={pipLink}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className={style.label}
|
||||
>
|
||||
{pipLink}
|
||||
</a>
|
||||
</p>
|
||||
<p className={style.flexNote}>
|
||||
Studio Clock<br />
|
||||
<a
|
||||
href={studioLink}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className={style.label}
|
||||
>
|
||||
{studioLink}
|
||||
</a>
|
||||
</p>
|
||||
eg. a lower third url with some custom parameters
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className={style.labelNote} style={{ width: '30%' }}>
|
||||
Alias
|
||||
</td>
|
||||
<td className={style.labelNote}>Page URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>mylower</td>
|
||||
<td>lower?bg=ff2&text=f00&size=0.6&transition=5</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
<span className={style.labelNote}>
|
||||
URLs to be changed dynamically
|
||||
</span>
|
||||
<br />
|
||||
eg. an unattended screen that you would need to change route from
|
||||
the app
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className={style.labelNote} style={{ width: '30%' }}>
|
||||
Alias
|
||||
</td>
|
||||
<td className={style.labelNote}>Page URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>thirdfloor</td>
|
||||
<td>public</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
className={style.inlineAliasPlaceholder}
|
||||
style={{ padding: '0.5em 0' }}
|
||||
>
|
||||
<span className={style.labelNote}>Alias</span>
|
||||
<span className={style.labelNote}>Page URL</span>
|
||||
</div>
|
||||
{aliases.map((alias, index) => (
|
||||
<div key={alias.id}>
|
||||
<div className={style.inlineAlias}>
|
||||
<Input
|
||||
size='sm'
|
||||
variant='flushed'
|
||||
name='Alias'
|
||||
placeholder='URL Alias'
|
||||
autoComplete='off'
|
||||
value={alias.alias}
|
||||
isInvalid={alias.aliasError}
|
||||
onChange={(event) =>
|
||||
handleChange(index, 'alias', event.target.value)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
size='sm'
|
||||
fontSize={'0.75em'}
|
||||
variant='flushed'
|
||||
name='URL'
|
||||
placeholder='URL (portion after ontime Port)'
|
||||
autoComplete='off'
|
||||
value={alias.pathAndParams}
|
||||
isInvalid={alias.urlError}
|
||||
onChange={(event) =>
|
||||
handleChange(index, 'pathAndParams', event.target.value)
|
||||
}
|
||||
/>
|
||||
<Tooltip label={`Test /${alias.pathAndParams}`} openDelay={500}>
|
||||
<a
|
||||
href='#!'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleLink(`http://${host}/${alias.pathAndParams}`);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label='Enable alias' openDelay={500}>
|
||||
<IconButton
|
||||
size='xs'
|
||||
icon={<FiSun />}
|
||||
colorScheme='blue'
|
||||
variant={alias.enabled ? null : 'outline'}
|
||||
onClick={() => setEnabled(alias.id, !alias.enabled)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label='Delete alias' openDelay={500}>
|
||||
<IconButton
|
||||
size='xs'
|
||||
icon={<FiMinus />}
|
||||
colorScheme='red'
|
||||
onClick={() => deleteAlias(alias.id)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{alias.aliasError ? (
|
||||
<div
|
||||
className={style.error}
|
||||
>{`Alias error: ${alias.aliasError}`}</div>
|
||||
) : null}
|
||||
{alias.urlError ? (
|
||||
<div
|
||||
className={style.error}
|
||||
>{`URL error: ${alias.urlError}`}</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<span>Manage custom aliases</span>
|
||||
<div className={style.modalInline}>
|
||||
<Input
|
||||
size='sm'
|
||||
name='URL'
|
||||
placeholder='A long URL'
|
||||
autoComplete='off'
|
||||
value={'A long URL'}
|
||||
onChange={(event) => {
|
||||
// Nothing here yet
|
||||
}}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<Input
|
||||
size='sm'
|
||||
name='Alias'
|
||||
placeholder='A nice alias'
|
||||
autoComplete='off'
|
||||
value={'A nice alias'}
|
||||
onChange={(event) => {
|
||||
// Nothing here yet
|
||||
}}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<IconButton
|
||||
size='sm'
|
||||
icon={<FiMinus />}
|
||||
colorScheme='red'
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className={style.separator} />
|
||||
<div className={style.modalInline}>
|
||||
<Input
|
||||
size='sm'
|
||||
name='URL'
|
||||
placeholder='URL'
|
||||
autoComplete='off'
|
||||
value={'URL'}
|
||||
onChange={(event) => {
|
||||
// Nothing here yet
|
||||
}}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<Input
|
||||
size='sm'
|
||||
name='Alias'
|
||||
placeholder='Alias'
|
||||
autoComplete='off'
|
||||
value={'Alias'}
|
||||
onChange={(event) => {
|
||||
// Nothing here yet
|
||||
}}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<IconButton
|
||||
size='sm'
|
||||
icon={<FiPlus />}
|
||||
colorScheme='blue'
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className={style.submitContainer}>
|
||||
<div
|
||||
className={style.inlineAliasPlaceholder}
|
||||
style={{ padding: '0.5em 0' }}
|
||||
>
|
||||
<Button
|
||||
size='xs'
|
||||
colorScheme='blue'
|
||||
type='submit'
|
||||
isLoading={submitting}
|
||||
disabled={true}
|
||||
variant='outline'
|
||||
onClick={() => addNew()}
|
||||
>
|
||||
Save
|
||||
Add new
|
||||
</Button>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</div>
|
||||
<SubmitContainer
|
||||
revert={revert}
|
||||
submitting={submitting}
|
||||
changed={changed}
|
||||
status={status}
|
||||
/>
|
||||
</form>
|
||||
</>
|
||||
</ModalBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,183 +1,170 @@
|
||||
import { ModalBody } from '@chakra-ui/modal';
|
||||
import { FormLabel, FormControl, Input, Button } from '@chakra-ui/react';
|
||||
import { getOSC, oscPlaceholderSettings, postOSC } from 'app/api/ontimeApi';
|
||||
import {
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
PinInput,
|
||||
PinInputField,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
getSettings,
|
||||
ontimePlaceholderSettings,
|
||||
postSettings,
|
||||
} from 'app/api/ontimeApi';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useFetch } from 'app/hooks/useFetch';
|
||||
import { OSC_SETTINGS } from 'app/api/apiConstants';
|
||||
import { APP_SETTINGS } from 'app/api/apiConstants';
|
||||
import style from './Modals.module.scss';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiEye } from 'react-icons/fi';
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
import { inputProps } from './modalHelper';
|
||||
|
||||
export default function AppSettingsModal() {
|
||||
const { data, status } = useFetch(OSC_SETTINGS, getOSC);
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const [formData, setFormData] = useState(oscPlaceholderSettings);
|
||||
const { data, status, refetch } = useFetch(APP_SETTINGS, getSettings);
|
||||
const { emitError, emitWarning } = useContext(LoggingContext);
|
||||
const [formData, setFormData] = useState(ontimePlaceholderSettings);
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [hidePin, setHidePin] = useState(true);
|
||||
|
||||
/**
|
||||
* Set formdata from server state
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (data == null) return;
|
||||
setFormData({ ...data });
|
||||
}, [data]);
|
||||
if (changed) return;
|
||||
setFormData({
|
||||
pinCode: data.pinCode,
|
||||
});
|
||||
}, [changed, data]);
|
||||
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
|
||||
const f = formData;
|
||||
let e = { status: false, message: '' };
|
||||
|
||||
// Validate fields
|
||||
if (f.port < 1024 || f.port > 65535) {
|
||||
// Port in incorrect range
|
||||
if (f.pinCode === '' || f.pinCode == null) {
|
||||
e.status = true;
|
||||
e.message += 'OSC IN Port in incorrect range (1024 - 65535)';
|
||||
} else if (f.portOut < 1024 || f.portOut > 65535) {
|
||||
// Port in incorrect range
|
||||
e.message += 'App pin code removed';
|
||||
} else {
|
||||
e.status = true;
|
||||
e.message += 'OSC OUT Port in incorrect range (1024 - 65535)';
|
||||
} else if (f.port === f.portOut) {
|
||||
// Cant use the same port
|
||||
e.status = true;
|
||||
e.message += 'OSC IN and OUT Ports cant be the same';
|
||||
e.message += 'App pin code added';
|
||||
}
|
||||
|
||||
// set fields with error
|
||||
if (e.status) {
|
||||
if (!e.status) {
|
||||
emitError(`Invalid Input: ${e.message}`);
|
||||
return;
|
||||
} else {
|
||||
await postSettings(formData);
|
||||
await refetch();
|
||||
emitWarning(e.message);
|
||||
setChanged(false);
|
||||
}
|
||||
|
||||
// Post here
|
||||
postOSC(formData);
|
||||
|
||||
setChanged(false);
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={submitHandler}>
|
||||
<ModalBody className={style.modalBody}>
|
||||
{status === 'success' && (
|
||||
<>
|
||||
<p className={style.notes}>
|
||||
Options related to the application
|
||||
<br />
|
||||
🔥 Changes take effect after app restart 🔥
|
||||
</p>
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
*/
|
||||
const revert = async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
};
|
||||
|
||||
<FormControl id='serverPort'>
|
||||
<FormLabel htmlFor='serverPort'>
|
||||
Viewer Port
|
||||
<span className={style.notes}>Port to access viewers</span>
|
||||
</FormLabel>
|
||||
<Input
|
||||
/**
|
||||
* Handles change of input field in local state
|
||||
* @param {string} field - object parameter to update
|
||||
* @param {string} value - new object parameter value
|
||||
*/
|
||||
const handleChange = (field, value) => {
|
||||
const temp = { ...formData };
|
||||
temp[field] = value;
|
||||
setFormData(temp);
|
||||
setChanged(true);
|
||||
};
|
||||
|
||||
const disableModal = status !== 'success';
|
||||
|
||||
return (
|
||||
<ModalBody className={style.modalBody}>
|
||||
<p className={style.notes}>
|
||||
Options related to the application
|
||||
<br />
|
||||
🔥 Changes take effect after app restart 🔥
|
||||
</p>
|
||||
<form onSubmit={submitHandler}>
|
||||
<div className={style.modalFields}>
|
||||
<div className={style.hSeparator}>General App Settings</div>
|
||||
<div className={style.modalInline}>
|
||||
<FormControl id='serverPort'>
|
||||
<FormLabel htmlFor='serverPort'>
|
||||
Viewer Port
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
Ontime is available at port
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Input
|
||||
{...inputProps}
|
||||
name='title'
|
||||
value={4001}
|
||||
disabled
|
||||
style={{ width: '6em', textAlign: 'center' }}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl id='editorPin'>
|
||||
<FormLabel htmlFor='editorPin'>
|
||||
Editor Pincode
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
Protect the editor with a Pincode
|
||||
</span>
|
||||
</FormLabel>
|
||||
<div className={style.pin}>
|
||||
<PinInput
|
||||
{...inputProps}
|
||||
type='alphanumeric'
|
||||
defaultValue=''
|
||||
value={formData.pinCode}
|
||||
mask={hidePin}
|
||||
isDisabled={disableModal}
|
||||
onChange={(value) => handleChange('pinCode', value)}
|
||||
>
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
</PinInput>
|
||||
<IconButton
|
||||
size='sm'
|
||||
name='title'
|
||||
placeholder='4001'
|
||||
autoComplete='off'
|
||||
value={4001}
|
||||
readOnly
|
||||
style={{ width: '6em', textAlign: 'center' }}
|
||||
colorScheme='blue'
|
||||
variant='ghost'
|
||||
icon={<FiEye />}
|
||||
aria-label='Editor pin code'
|
||||
onMouseDown={() => setHidePin(false)}
|
||||
onMouseUp={() => setHidePin(true)}
|
||||
isDisabled={disableModal}
|
||||
/>
|
||||
<span className={style.notes}>(Read Only Value)</span>
|
||||
</FormControl>
|
||||
<FormControl id='port'>
|
||||
<FormLabel htmlFor='port'>
|
||||
OSC In Port
|
||||
<span className={style.notes}>
|
||||
<br />
|
||||
App Control - Default 8888
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Input
|
||||
size='sm'
|
||||
name='port'
|
||||
placeholder='8888'
|
||||
autoComplete='off'
|
||||
type='number'
|
||||
value={formData.port}
|
||||
min='1024'
|
||||
max='65535'
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
port: parseInt(event.target.value),
|
||||
});
|
||||
}}
|
||||
isDisabled={submitting}
|
||||
style={{ width: '6em', textAlign: 'center' }}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className={style.modalInline}>
|
||||
<FormControl id='targetIP' width='auto'>
|
||||
<FormLabel htmlFor='targetIP'>
|
||||
OSC Out Target IP
|
||||
<span className={style.notes}>
|
||||
<br />
|
||||
App Feedback - Default 127.0.0.1
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Input
|
||||
size='sm'
|
||||
name='targetIP'
|
||||
placeholder='127.0.0.1'
|
||||
autoComplete='off'
|
||||
value={formData.targetIP}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
targetIP: event.target.value,
|
||||
});
|
||||
}}
|
||||
isDisabled={submitting}
|
||||
style={{ width: '12em', textAlign: 'right' }}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl id='portOut' width='auto'>
|
||||
<FormLabel htmlFor='portOut'>
|
||||
OSC Out Port
|
||||
<span className={style.notes}>
|
||||
<br />
|
||||
Default 9999
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Input
|
||||
size='sm'
|
||||
name='portOut'
|
||||
placeholder='9999'
|
||||
autoComplete='off'
|
||||
type='number'
|
||||
value={formData.portOut}
|
||||
min='1024'
|
||||
max='65535'
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
portOut: parseInt(event.target.value),
|
||||
});
|
||||
}}
|
||||
isDisabled={submitting}
|
||||
style={{ width: '6em', textAlign: 'left' }}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className={style.submitContainer}>
|
||||
<Button
|
||||
colorScheme='blue'
|
||||
type='submit'
|
||||
isLoading={submitting}
|
||||
disabled={!changed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</FormControl>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</div>
|
||||
<SubmitContainer
|
||||
revert={revert}
|
||||
submitting={submitting}
|
||||
changed={changed}
|
||||
status={status}
|
||||
/>
|
||||
</form>
|
||||
</>
|
||||
</ModalBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
import { ModalBody } from '@chakra-ui/modal';
|
||||
import {
|
||||
FormLabel,
|
||||
FormControl,
|
||||
Input,
|
||||
Button,
|
||||
Textarea,
|
||||
} from '@chakra-ui/react';
|
||||
import { FormLabel, Input, Textarea } from '@chakra-ui/react';
|
||||
import { fetchEvent, postEvent } from 'app/api/eventApi';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useFetch } from 'app/hooks/useFetch';
|
||||
import { EVENT_TABLE } from 'app/api/apiConstants';
|
||||
import style from './Modals.module.scss';
|
||||
import { eventPlaceholderSettings } from '../../app/api/ontimeApi';
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
import { inputProps } from './modalHelper';
|
||||
|
||||
export default function SettingsModal() {
|
||||
const { data, status } = useFetch(EVENT_TABLE, fetchEvent);
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
url: '',
|
||||
publicInfo: '',
|
||||
backstageInfo: '',
|
||||
endMessage: '',
|
||||
});
|
||||
const { data, status, refetch } = useFetch(EVENT_TABLE, fetchEvent);
|
||||
const [formData, setFormData] = useState(eventPlaceholderSettings);
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
/**
|
||||
* Set formdata from server state
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (data == null) return;
|
||||
if (changed) return;
|
||||
|
||||
setFormData({
|
||||
title: data.title,
|
||||
@@ -34,8 +29,11 @@ export default function SettingsModal() {
|
||||
backstageInfo: data.backstageInfo,
|
||||
endMessage: data.endMessage,
|
||||
});
|
||||
}, [data]);
|
||||
}, [changed, data]);
|
||||
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
@@ -46,133 +44,128 @@ export default function SettingsModal() {
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
*/
|
||||
const revert = async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles change of input field in local state
|
||||
* @param {string} field - object parameter to update
|
||||
* @param {string} value - new object parameter value
|
||||
*/
|
||||
const handleChange = (field, value) => {
|
||||
const temp = { ...formData };
|
||||
temp[field] = value;
|
||||
setFormData(temp);
|
||||
setChanged(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalBody className={style.modalBody}>
|
||||
<p className={style.notes}>
|
||||
Options related to the running event
|
||||
<br />
|
||||
Affects rendered views
|
||||
</p>
|
||||
<form onSubmit={submitHandler}>
|
||||
<ModalBody className={style.modalBody}>
|
||||
{status === 'success' && (
|
||||
<>
|
||||
<p className={style.notes}>
|
||||
Options related to the running event
|
||||
<br />
|
||||
Affect rendered views
|
||||
</p>
|
||||
|
||||
<FormControl id='title'>
|
||||
<FormLabel htmlFor='title'>Event Title</FormLabel>
|
||||
<Input
|
||||
size='sm'
|
||||
maxLength={35}
|
||||
name='title'
|
||||
placeholder='Event Title'
|
||||
autoComplete='off'
|
||||
value={formData.title}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({ ...formData, title: event.target.value });
|
||||
}}
|
||||
isDisabled={submitting}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl id='url'>
|
||||
<FormLabel htmlFor='url'>
|
||||
Event URL
|
||||
<span className={style.notes}>
|
||||
(shown as a QR code in some views)
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Input
|
||||
size='sm'
|
||||
name='url'
|
||||
placeholder='www.onsite.no'
|
||||
autoComplete='off'
|
||||
value={formData.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({ ...formData, url: event.target.value });
|
||||
}}
|
||||
isDisabled={submitting}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl id='pubInfo'>
|
||||
<FormLabel htmlFor='pubInfo'>Public Info</FormLabel>
|
||||
<Textarea
|
||||
size='sm'
|
||||
name='pubInfo'
|
||||
placeholder='Information to be shown on public screens'
|
||||
autoComplete='off'
|
||||
value={formData.publicInfo}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
publicInfo: event.target.value,
|
||||
});
|
||||
}}
|
||||
isDisabled={submitting}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl id='backstageInfo'>
|
||||
<FormLabel htmlFor='backstageInfo'>Backstage Info</FormLabel>
|
||||
<Textarea
|
||||
size='sm'
|
||||
name='backstageInfo'
|
||||
placeholder='Information to be shown on backstage screens'
|
||||
autoComplete='off'
|
||||
resize={false}
|
||||
value={formData.backstageInfo}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
backstageInfo: event.target.value,
|
||||
});
|
||||
}}
|
||||
isDisabled={submitting}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl id='endMessage'>
|
||||
<FormLabel htmlFor='endMessage'>
|
||||
End Message
|
||||
<span className={style.notes}>
|
||||
Shown on presenter view when time is finished
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Input
|
||||
size='sm'
|
||||
maxLength={30}
|
||||
name='endMessage'
|
||||
placeholder='Empty message shows elapsed time'
|
||||
autoComplete='off'
|
||||
value={formData.endMessage}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
endMessage: event.target.value,
|
||||
});
|
||||
}}
|
||||
isDisabled={submitting}
|
||||
/>
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
<div className={style.submitContainer}>
|
||||
<Button
|
||||
colorScheme='blue'
|
||||
type='submit'
|
||||
isLoading={submitting}
|
||||
disabled={!changed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<div className={style.modalFields}>
|
||||
<div className={style.hSeparator}>Event Data</div>
|
||||
<div className={style.spacedEntry}>
|
||||
<FormLabel htmlFor='title'>Event Title</FormLabel>
|
||||
<Input
|
||||
{...inputProps}
|
||||
maxLength={35}
|
||||
name='title'
|
||||
placeholder='Event Title'
|
||||
value={formData.title}
|
||||
onChange={(event) => handleChange('title', event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<div className={style.hSeparator}>Additional Screen Info</div>
|
||||
<div className={style.spacedEntry}>
|
||||
<FormLabel htmlFor='url'>
|
||||
Event URL
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
Shown as a QR code in some views
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Input
|
||||
{...inputProps}
|
||||
name='url'
|
||||
placeholder='www.onsite.no'
|
||||
value={formData.url}
|
||||
onChange={(event) => handleChange('url', event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.spacedEntry}>
|
||||
<FormLabel htmlFor='pubInfo'>
|
||||
Public Info
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
Information to be shown on public screens
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Textarea
|
||||
{...inputProps}
|
||||
name='pubInfo'
|
||||
placeholder='Information to be shown on public screens'
|
||||
value={formData.publicInfo}
|
||||
onChange={(event) =>
|
||||
handleChange('publicInfo', event.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.spacedEntry}>
|
||||
<FormLabel htmlFor='backstageInfo'>
|
||||
Backstage Info
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
Information to be shown on backstage screens
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Textarea
|
||||
{...inputProps}
|
||||
name='backstageInfo'
|
||||
placeholder='Information to be shown on backstage screens'
|
||||
resize={false}
|
||||
value={formData.backstageInfo}
|
||||
onChange={(event) =>
|
||||
handleChange('backstageInfo', event.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.spacedEntry}>
|
||||
<FormLabel htmlFor='endMessage'>
|
||||
End Message
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
Shown on presenter view when time is finished
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Input
|
||||
{...inputProps}
|
||||
maxLength={30}
|
||||
name='endMessage'
|
||||
placeholder='Empty message shows elapsed time'
|
||||
value={formData.endMessage}
|
||||
onChange={(event) =>
|
||||
handleChange('endMessage', event.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SubmitContainer
|
||||
revert={revert}
|
||||
submitting={submitting}
|
||||
changed={changed}
|
||||
status={status}
|
||||
/>
|
||||
</form>
|
||||
</>
|
||||
</ModalBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { ModalBody } from '@chakra-ui/modal';
|
||||
import {
|
||||
FormLabel,
|
||||
FormControl,
|
||||
Input,
|
||||
Button,
|
||||
Switch,
|
||||
} from '@chakra-ui/react';
|
||||
import { FormControl, FormLabel, Input, Switch } from '@chakra-ui/react';
|
||||
import {
|
||||
getInfo,
|
||||
httpPlaceholder,
|
||||
@@ -17,23 +11,23 @@ import { useFetch } from 'app/hooks/useFetch';
|
||||
import { APP_TABLE } from 'app/api/apiConstants';
|
||||
import style from './Modals.module.scss';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
import { FiInfo } from 'react-icons/fi';
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
import { inputProps } from './modalHelper';
|
||||
|
||||
export default function IntegrationSettingsModal() {
|
||||
const { data, status } = useFetch(APP_TABLE, getInfo);
|
||||
const { data, status, refetch } = useFetch(APP_TABLE, getInfo);
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const [formData, setFormData] = useState(httpPlaceholder);
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const ready = status === 'success';
|
||||
const integrationInputProps = {
|
||||
size: 'sm',
|
||||
autoComplete: 'off',
|
||||
isDisabled: submitting || !ready,
|
||||
};
|
||||
|
||||
/**
|
||||
* Set formdata from server state
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (data == null) return;
|
||||
if (changed) return;
|
||||
|
||||
setFormData({
|
||||
onLoad: data?.onLoad,
|
||||
@@ -42,8 +36,11 @@ export default function IntegrationSettingsModal() {
|
||||
onPause: data?.onPause,
|
||||
onStop: data?.onStop,
|
||||
});
|
||||
}, [data]);
|
||||
}, [changed, data]);
|
||||
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -54,283 +51,312 @@ export default function IntegrationSettingsModal() {
|
||||
if (e.status) {
|
||||
emitError(`Invalid Input: ${e.message}`);
|
||||
return;
|
||||
} else {
|
||||
await postInfo(f);
|
||||
setChanged(false);
|
||||
}
|
||||
|
||||
// Post here
|
||||
postInfo(f);
|
||||
|
||||
setChanged(false);
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={submitHandler}>
|
||||
<ModalBody
|
||||
className={ready ? style.modalBody : style.modalBodyDisabled}
|
||||
>
|
||||
<>
|
||||
<p className={style.notes}>
|
||||
Integrate with third party over an HTTP API
|
||||
<br />
|
||||
🔥 Changes take effect after app restart 🔥
|
||||
</p>
|
||||
<div className={style.highNotes}>
|
||||
<p>
|
||||
Add HTTP messages that ontime will send during the app lifecycle
|
||||
</p>
|
||||
<p>
|
||||
You can use the variables below to pass data directly from
|
||||
ontime eg:
|
||||
<span className={style.emNote}>
|
||||
http://127.0.0.1:8088/API/?setHeadline=<b>$title</b>
|
||||
&setSub=<b>$presenter</b>
|
||||
</span>
|
||||
</p>
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
*/
|
||||
const revert = async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
};
|
||||
|
||||
<table>
|
||||
// Todo: make change handler
|
||||
// Todo: toggle between GET / POST
|
||||
// Todo: add test button
|
||||
// Todo: enabled should be button
|
||||
// Todo: add friendly placeholder to input
|
||||
|
||||
return (
|
||||
<ModalBody className={style.modalBody}>
|
||||
<p className={style.notes}>
|
||||
Integrate with third party over an HTTP API
|
||||
<br />
|
||||
🔥 Changes take effect after app restart 🔥
|
||||
</p>
|
||||
<form onSubmit={submitHandler}>
|
||||
<div className={style.modalFields}>
|
||||
<div className={style.hSeparator}>Ontime event cycle</div>
|
||||
<div className={style.blockNotes}>
|
||||
<span className={style.inlineFlex}>
|
||||
<FiInfo color='#2b6cb0' fontSize={'2em'} />
|
||||
Add HTTP messages that ontime will send during the event cycle
|
||||
</span>
|
||||
<span className={style.labelNote}>
|
||||
You can use variables in the HTTP request URL to send data from
|
||||
ontime
|
||||
</span>
|
||||
<span className={style.emNote}>
|
||||
http://127.0.0.1:8088/API/?setHeadline=
|
||||
<span className={style.labelNoteInline}>$title</span>
|
||||
&setSub=<span className={style.labelNoteInline}>$presenter</span>
|
||||
</span>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className={style.labelNote} style={{ width: '30%' }}>
|
||||
Variable
|
||||
</td>
|
||||
<td className={style.labelNote}>Value</td>
|
||||
</tr>
|
||||
{ontimeVars.map((v) => (
|
||||
<tr>
|
||||
<td className={style.noteItem}>{v.name}</td>
|
||||
<td className={style.labelNote}>{v.name}</td>
|
||||
<td>{v.description}</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<FormLabel>
|
||||
On Load
|
||||
<span className={style.notes}>When a new event loads</span>
|
||||
</FormLabel>
|
||||
<FormControl id='onLoad' className={style.modalInline}>
|
||||
<Input
|
||||
{...integrationInputProps}
|
||||
name='onLoadURL'
|
||||
value={formData?.onLoad?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onLoad: {
|
||||
...formData.onLoad,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onLoadEnable'
|
||||
value={formData?.onLoad?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onLoad: {
|
||||
...formData.onLoad,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel>
|
||||
On Start
|
||||
<span className={style.notes}>
|
||||
When an timer starts / resumes
|
||||
</span>
|
||||
</FormLabel>
|
||||
<FormControl id='onStart' className={style.modalInline}>
|
||||
<Input
|
||||
{...integrationInputProps}
|
||||
name='onStartURL'
|
||||
value={formData?.onStart?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStart: {
|
||||
...formData.onStart,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onStartEnable'
|
||||
value={formData?.onStart?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStart: {
|
||||
...formData.onStart,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel>
|
||||
On Update
|
||||
<span className={style.notes}>At every clock tick</span>
|
||||
</FormLabel>
|
||||
<FormControl id='onUpdate' className={style.modalInline}>
|
||||
<Input
|
||||
{...integrationInputProps}
|
||||
name='onUpdateURL'
|
||||
value={formData?.onUpdate?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onUpdate: {
|
||||
...formData.onUpdate,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onUpdateEnable'
|
||||
value={formData?.onUpdate?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onUpdate: {
|
||||
...formData.onUpdate,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel>
|
||||
On Pause
|
||||
<span className={style.notes}>When a timer pauses</span>
|
||||
</FormLabel>
|
||||
<FormControl id='onPause' className={style.modalInline}>
|
||||
<Input
|
||||
{...integrationInputProps}
|
||||
name='onPauseURL'
|
||||
value={formData?.onPause?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onPause: {
|
||||
...formData.onPause,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onPauseEnable'
|
||||
value={formData?.onPause?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onPause: {
|
||||
...formData.onPause,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel>
|
||||
On Stop
|
||||
<span className={style.notes}>When an event is unloaded</span>
|
||||
</FormLabel>
|
||||
<FormControl id='onStop' className={style.modalInline}>
|
||||
<Input
|
||||
{...integrationInputProps}
|
||||
name='onStopURL'
|
||||
value={formData?.onStop?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStop: {
|
||||
...formData.onStop,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onStopEnable'
|
||||
value={formData?.onStop?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStop: {
|
||||
...formData.onStop,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel>
|
||||
On Finish
|
||||
<span className={style.notes}>When an event is finished</span>
|
||||
</FormLabel>
|
||||
<FormControl id='onFinish' className={style.modalInline}>
|
||||
<Input
|
||||
{...integrationInputProps}
|
||||
name='onFinishURL'
|
||||
value={formData?.onFinish?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStop: {
|
||||
...formData.onFinish,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onFinishEnable'
|
||||
value={formData?.onFinish?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStop: {
|
||||
...formData.onStop,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</>
|
||||
</>
|
||||
<div className={style.submitContainer}>
|
||||
<Button
|
||||
colorScheme='blue'
|
||||
type='submit'
|
||||
isLoading={submitting}
|
||||
disabled={!changed || !ready}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<div className={style.hSeparator}>Send HTTP</div>
|
||||
<FormLabel style={{paddingLeft:'0.5em'}}>
|
||||
On Load
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
When a new event loads
|
||||
</span>
|
||||
</FormLabel>
|
||||
<div className={style.modalInline}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
name='onLoadURL'
|
||||
value={formData?.onLoad?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onLoad: {
|
||||
...formData.onLoad,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onLoadEnable'
|
||||
value={formData?.onLoad?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onLoad: {
|
||||
...formData.onLoad,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FormLabel style={{paddingLeft:'0.5em'}}>
|
||||
On Start
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
When an timer starts / resumes{' '}
|
||||
</span>
|
||||
</FormLabel>
|
||||
<div className={style.modalInline}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
name='onStartURL'
|
||||
value={formData?.onStart?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStart: {
|
||||
...formData.onStart,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onStartEnable'
|
||||
value={formData?.onStart?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStart: {
|
||||
...formData.onStart,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FormLabel style={{paddingLeft:'0.5em'}}>
|
||||
On Update
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
At every clock tick
|
||||
</span>
|
||||
</FormLabel>
|
||||
<FormControl id='onUpdate' className={style.modalInline}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
name='onUpdateURL'
|
||||
value={formData?.onUpdate?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onUpdate: {
|
||||
...formData.onUpdate,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onUpdateEnable'
|
||||
value={formData?.onUpdate?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onUpdate: {
|
||||
...formData.onUpdate,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel style={{paddingLeft:'0.5em'}}>
|
||||
On Pause
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
When a timer pauses
|
||||
</span>
|
||||
</FormLabel>
|
||||
<FormControl id='onPause' className={style.modalInline}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
name='onPauseURL'
|
||||
value={formData?.onPause?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onPause: {
|
||||
...formData.onPause,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onPauseEnable'
|
||||
value={formData?.onPause?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onPause: {
|
||||
...formData.onPause,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel style={{paddingLeft:'0.5em'}}>
|
||||
On Stop
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
When an event is unloaded
|
||||
</span>
|
||||
</FormLabel>
|
||||
<FormControl id='onStop' className={style.modalInline}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
name='onStopURL'
|
||||
value={formData?.onStop?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStop: {
|
||||
...formData.onStop,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onStopEnable'
|
||||
value={formData?.onStop?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStop: {
|
||||
...formData.onStop,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel style={{paddingLeft:'0.5em'}}>
|
||||
On Finish
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
When an event is finished
|
||||
</span>
|
||||
</FormLabel>
|
||||
<FormControl id='onFinish' className={style.modalInline}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
name='onFinishURL'
|
||||
value={formData?.onFinish?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStop: {
|
||||
...formData.onFinish,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onFinishEnable'
|
||||
value={formData?.onFinish?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStop: {
|
||||
...formData.onStop,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<SubmitContainer
|
||||
revert={revert}
|
||||
submitting={submitting}
|
||||
changed={changed}
|
||||
status={status}
|
||||
/>
|
||||
</form>
|
||||
</>
|
||||
</ModalBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
} from '@chakra-ui/modal';
|
||||
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/tabs';
|
||||
import EventSettingsModal from './EventSettingsModal';
|
||||
import AppSettingsModal from './AppSettingsModal';
|
||||
import OscSettingsModal from './OscSettingsModal';
|
||||
import AliasesModal from './AliasesModal';
|
||||
import IntegrationSettingsModal from './IntegrationSettingsModal';
|
||||
import AppSettingsModal from './AppSettingsModal';
|
||||
|
||||
export default function ModalManager(props) {
|
||||
const { isOpen, onClose } = props;
|
||||
@@ -20,7 +21,8 @@ export default function ModalManager(props) {
|
||||
onClose={onClose}
|
||||
closeOnOverlayClick={false}
|
||||
motionPreset={'slideInBottom'}
|
||||
size='lg'
|
||||
size='xl'
|
||||
scrollBehavior='inside'
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
@@ -29,21 +31,25 @@ export default function ModalManager(props) {
|
||||
|
||||
<Tabs size='sm' isLazy>
|
||||
<TabList>
|
||||
<Tab style={{ fontSize: '0.9em' }}>App Settings</Tab>
|
||||
<Tab style={{ fontSize: '0.9em' }}>Event Data</Tab>
|
||||
<Tab style={{ fontSize: '0.9em' }}>Application Settings</Tab>
|
||||
<Tab style={{ fontSize: '0.9em' }}>URL Aliases</Tab>
|
||||
<Tab style={{ fontSize: '0.9em' }}>OSC</Tab>
|
||||
{/*<Tab style={{ fontSize: '0.9em' }}>Integration</Tab>*/}
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<EventSettingsModal />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<AppSettingsModal />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<EventSettingsModal />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<AliasesModal />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<OscSettingsModal />
|
||||
</TabPanel>
|
||||
{/*<TabPanel>*/}
|
||||
{/* <IntegrationSettingsModal />*/}
|
||||
{/*</TabPanel>*/}
|
||||
|
||||
@@ -1,83 +1,155 @@
|
||||
@use '../../styles/variables' as *;
|
||||
@use '../../styles/main' as *;
|
||||
|
||||
//////////////////////////////////// main
|
||||
|
||||
.modalBody,
|
||||
.modalBodyDisabled {
|
||||
.modalBody {
|
||||
font-weight: 400;
|
||||
|
||||
.notes {
|
||||
font-weight: 400;
|
||||
color: $light-bg;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 4em;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border: 1px solid $light-bg-transparent;
|
||||
width: 50%;
|
||||
margin: 0.5em auto;
|
||||
.modalFields {
|
||||
min-height: 45vh;
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
scrollbar-color: rgba($light-bg, 0.35) rgba($light-bg, 0.15);
|
||||
padding-right: 6px;
|
||||
|
||||
label {
|
||||
//font-weight: 400;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.inlineAlias,
|
||||
.inlineAliasPlaceholder {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr 1em 1.5em 1.5em;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.error {
|
||||
font-size: 0.8em;
|
||||
color: $error-red;
|
||||
}
|
||||
|
||||
.inlineAliasPlaceholder {
|
||||
grid-template-columns: 20% 1fr 4em;
|
||||
|
||||
.placeholder {
|
||||
background: $light-text;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba($light-bg, 0.15);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba($light-bg, 0.35);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba($light-bg, 0.45);
|
||||
}
|
||||
|
||||
.modalInline {
|
||||
display: flex;
|
||||
gap: 2em;
|
||||
align-items: center;
|
||||
padding: 0 0.5em 0.5em 0.5em;
|
||||
}
|
||||
|
||||
.spacedEntry {
|
||||
padding: 0 0.5em 0.5em 0.5em;
|
||||
}
|
||||
|
||||
.pin {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
border-radius: 50%;
|
||||
|
||||
input {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.submitContainer {
|
||||
margin-top: 2em;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
button {
|
||||
margin-top: 1em;
|
||||
}
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.modalBody > *,
|
||||
.modalBodyDisabled > * {
|
||||
.modalBody > * {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
//////////////////////////////////// notes
|
||||
p {
|
||||
&.notes {
|
||||
text-align: center;
|
||||
border-color: $light-bg-transparent;
|
||||
border-width: 0 2px;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 1em;
|
||||
text-align: center;
|
||||
border-color: $light-bg-transparent;
|
||||
border-width: 0 2px;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
&.notes {
|
||||
font-size: 0.8em;
|
||||
font-size: 0.9em;
|
||||
padding-left: 0.4em;
|
||||
}
|
||||
}
|
||||
|
||||
.highNotes {
|
||||
background-color: $light-text;
|
||||
.blockNotes {
|
||||
background-color: $bg-gray;
|
||||
margin: 1em 0;
|
||||
padding: 0.3em;
|
||||
font-size: 0.9em;
|
||||
padding: 0.5em;
|
||||
font-size: 0.8em;
|
||||
border-radius: 2px;
|
||||
|
||||
table {
|
||||
background-color: $light-text;
|
||||
background-color: #fff;
|
||||
border-left: 4px solid lighten($ontime-pink, 5%);
|
||||
width: 100%;
|
||||
margin-top: 0.3em;
|
||||
margin: 0.5em 0;
|
||||
border-radius: 2px;
|
||||
|
||||
:first-child {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
td {
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
.noteItem {
|
||||
user-select: text;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
.flexNote {
|
||||
user-select: text;
|
||||
padding-bottom: 0.3em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.emNote {
|
||||
@@ -87,13 +159,18 @@ span {
|
||||
}
|
||||
}
|
||||
|
||||
// Define style for a link
|
||||
a {
|
||||
&::after {
|
||||
content: ' \2197';
|
||||
color: $accent;
|
||||
}
|
||||
&:hover {
|
||||
color: $accent;
|
||||
}
|
||||
.labelNote {
|
||||
color: $light-bg;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.labelNoteInline {
|
||||
color: $light-bg;
|
||||
}
|
||||
|
||||
.inlineFlex {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { ModalBody } from '@chakra-ui/modal';
|
||||
import { FormControl, FormLabel, Input } from '@chakra-ui/react';
|
||||
import { getOSC, oscPlaceholderSettings, postOSC } from 'app/api/ontimeApi';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useFetch } from 'app/hooks/useFetch';
|
||||
import { OSC_SETTINGS } from 'app/api/apiConstants';
|
||||
import style from './Modals.module.scss';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
import { inputProps, portInputProps } from './modalHelper';
|
||||
|
||||
|
||||
export default function OscSettingsModal() {
|
||||
const { data, status, refetch } = useFetch(OSC_SETTINGS, getOSC);
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const [formData, setFormData] = useState(oscPlaceholderSettings);
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
/**
|
||||
* Set formdata from server state
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (data == null) return;
|
||||
if (changed) return;
|
||||
setFormData({ ...data });
|
||||
}, [changed, data]);
|
||||
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
|
||||
const f = formData;
|
||||
let e = { status: false, message: '' };
|
||||
|
||||
// Validate fields
|
||||
if (f.port < 1024 || f.port > 65535) {
|
||||
// Port in incorrect range
|
||||
e.status = true;
|
||||
e.message += 'OSC IN Port in incorrect range (1024 - 65535)';
|
||||
} else if (f.portOut < 1024 || f.portOut > 65535) {
|
||||
// Port in incorrect range
|
||||
e.status = true;
|
||||
e.message += 'OSC OUT Port in incorrect range (1024 - 65535)';
|
||||
} else if (f.port === f.portOut) {
|
||||
// Cant use the same port
|
||||
e.status = true;
|
||||
e.message += 'OSC IN and OUT Ports cant be the same';
|
||||
}
|
||||
|
||||
// set fields with error
|
||||
if (e.status) {
|
||||
emitError(`Invalid Input: ${e.message}`);
|
||||
} else {
|
||||
// Post here
|
||||
await postOSC(formData);
|
||||
setChanged(false);
|
||||
}
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
*/
|
||||
const revert = async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles change of input field in local state
|
||||
* @param {string} field - object parameter to update
|
||||
* @param {(string | number)} value - new object parameter value
|
||||
*/
|
||||
const handleChange = (field, value) => {
|
||||
const temp = { ...formData };
|
||||
temp[field] = value;
|
||||
setFormData(temp);
|
||||
setChanged(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalBody className={style.modalBody}>
|
||||
<p className={style.notes}>
|
||||
Options related to Open Sound Control
|
||||
<br />
|
||||
🔥 Changes take effect after app restart 🔥
|
||||
</p>
|
||||
<form onSubmit={submitHandler}>
|
||||
<div className={style.modalFields}>
|
||||
<div className={style.hSeparator}>OSC Input (control)</div>
|
||||
<div className={style.spacedEntry}>
|
||||
<FormLabel htmlFor='port'>
|
||||
OSC In Port
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
Open port for 3rd party control over OSC - Default 8888
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Input
|
||||
{...portInputProps}
|
||||
name='port'
|
||||
placeholder='8888'
|
||||
value={formData.port}
|
||||
onChange={(event) =>
|
||||
handleChange('port', parseInt(event.target.value))
|
||||
}
|
||||
style={{ width: '6em', textAlign: 'center' }}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.hSeparator}>OSC Output (feedback)</div>
|
||||
<div className={style.modalInline}>
|
||||
<FormControl id='targetIP'>
|
||||
<FormLabel htmlFor='targetIP'>
|
||||
OSC Out Target IP
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
Default 127.0.0.1
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Input
|
||||
{...inputProps}
|
||||
size='sm'
|
||||
name='targetIP'
|
||||
placeholder='127.0.0.1'
|
||||
autoComplete='off'
|
||||
value={formData.targetIP}
|
||||
onChange={(event) =>
|
||||
handleChange('targetIP', event.target.value)
|
||||
}
|
||||
isDisabled={submitting}
|
||||
style={{ width: '12em', textAlign: 'right' }}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl id='portOut'>
|
||||
<FormLabel htmlFor='portOut'>
|
||||
OSC Out Port
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
Default 9999
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Input
|
||||
{...portInputProps}
|
||||
name='portOut'
|
||||
placeholder='9999'
|
||||
value={formData.portOut}
|
||||
onChange={(event) =>
|
||||
handleChange('portOut', parseInt(event.target.value))
|
||||
}
|
||||
style={{ width: '6em', textAlign: 'left' }}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<SubmitContainer
|
||||
revert={revert}
|
||||
submitting={submitting}
|
||||
changed={changed}
|
||||
status={status}
|
||||
/>
|
||||
</form>
|
||||
</ModalBody>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import style from './Modals.module.scss';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function SubmitContainer(props) {
|
||||
const { submitting, changed, revert, status } = props;
|
||||
|
||||
return (
|
||||
<div className={style.submitContainer}>
|
||||
<Button
|
||||
type='submit'
|
||||
isDisabled={submitting || !changed}
|
||||
variant='ghosted'
|
||||
onClick={() => revert()}
|
||||
>
|
||||
Revert
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme='blue'
|
||||
type='submit'
|
||||
isLoading={submitting}
|
||||
disabled={!changed || status !== 'success'}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SubmitContainer.propTypes = {
|
||||
submitting: PropTypes.bool,
|
||||
changed: PropTypes.bool,
|
||||
status: PropTypes.string,
|
||||
revert: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
export const inputProps = {
|
||||
size: 'sm',
|
||||
autoComplete: 'off',
|
||||
};
|
||||
|
||||
export const portInputProps = {
|
||||
...inputProps,
|
||||
type: 'number',
|
||||
min: '1024',
|
||||
max: '65535',
|
||||
};
|
||||
Reference in New Issue
Block a user