refactor forms to not use ant Form component; split server and instance details forms into their own components

This commit is contained in:
gingervitis 2021-01-28 03:08:57 -08:00
parent 04926d53e1
commit 5f70c77458
13 changed files with 403 additions and 392 deletions

View File

@ -24,9 +24,25 @@ export const SUCCESS_STATES = {
}; };
// CONFIG API ENDPOINTS // CONFIG API ENDPOINTS
export const API_VIDEO_VARIANTS = '/video/streamoutputvariants'; export const API_CUSTOM_CONTENT = '/pagecontent';
export const API_VIDEO_SEGMENTS = '/video/streamlatencylevel'; export const API_FFMPEG = '/ffmpegpath';
export const API_INSTANCE_URL = '/serverurl';
export const API_LOGO = '/logo';
export const API_NSFW_SWITCH = '/nsfw';
export const API_RTMP_PORT = '/rtmpserverport';
export const API_S3_INFO = '/s3';
export const API_SERVER_SUMMARY = '/serversummary';
export const API_SERVER_TITLE = '/servertitle';
export const API_SOCIAL_HANDLES = '/socialhandles'; export const API_SOCIAL_HANDLES = '/socialhandles';
export const API_STREAM_KEY = '/key';
export const API_STREAM_TITLE = '/streamtitle';
export const API_TAGS = '/tags';
export const API_USERNAME = '/name';
export const API_VIDEO_SEGMENTS = '/video/streamlatencylevel';
export const API_VIDEO_VARIANTS = '/video/streamoutputvariants';
export const API_WEB_PORT = '/webserverport';
export const API_YP_SWITCH = '/directoryenabled';
export async function postConfigUpdateToAPI(args: ApiPostArgs) { export async function postConfigUpdateToAPI(args: ApiPostArgs) {
const { const {
@ -47,176 +63,119 @@ export async function postConfigUpdateToAPI(args: ApiPostArgs) {
} }
} }
// Creating this so that it'll be easier to change values in one place, rather than looking for places to change it in a sea of JSX.
// key is the input's `fieldName` // Some default props to help build out a TextField
// the structure of this mirrors config data export const TEXTFIELD_PROPS_USERNAME = {
export const TEXTFIELD_DEFAULTS = { apiPath: API_USERNAME,
instanceDetails: { configPath: 'instanceDetails',
// user name
name: {
apiPath: '/name',
defaultValue: '',
maxLength: TEXT_MAXLENGTH, maxLength: TEXT_MAXLENGTH,
placeholder: 'username', placeholder: 'username',
label: 'User name', label: 'User name',
tip: 'Who are you? What name do you want viewers to know you?', tip: 'Who are you? What name do you want viewers to know you?',
}, };
export const TEXTFIELD_PROPS_SERVER_TITLE = {
// like "goth land" apiPath: API_SERVER_TITLE,
title: {
apiPath: '/servertitle',
defaultValue: '',
maxLength: TEXT_MAXLENGTH, maxLength: TEXT_MAXLENGTH,
placeholder: 'Owncast site name', placeholder: 'Owncast site name', // like "gothland"
label: 'Server Name', label: 'Server Name',
tip: 'The name of your Owncast server', tip: 'The name of your Owncast server',
}, };
export const TEXTFIELD_PROPS_STREAM_TITLE = {
apiPath: API_STREAM_TITLE,
streamTitle: {
apiPath: '/streamtitle',
defaultValue: '',
maxLength: TEXT_MAXLENGTH, maxLength: TEXT_MAXLENGTH,
placeholder: 'Doing cool things...', placeholder: 'Doing cool things...',
label: 'Stream Title', label: 'Stream Title',
tip: 'What is your stream about today?', tip: 'What is your stream about today?',
}, };
export const TEXTFIELD_PROPS_SERVER_SUMMARY = {
summary: { apiPath: API_SERVER_SUMMARY,
apiPath: '/serversummary',
defaultValue: '',
maxLength: 500, maxLength: 500,
placeholder: 'Summary', placeholder: 'Summary',
label: 'Summary', label: 'Summary',
tip: 'A brief blurb about what your stream is about.', tip: 'A brief blurb about what your stream is about.',
}, };
export const TEXTFIELD_PROPS_LOGO = {
logo: { apiPath: API_LOGO,
apiPath: '/logo',
defaultValue: '',
maxLength: 255, maxLength: 255,
placeholder: '/img/mylogo.png', placeholder: '/img/mylogo.png',
label: 'Logo', label: 'Logo',
tip: 'Path to your logo from website root. We recommend that you use a square image that is at least 256x256. (upload functionality coming soon)', tip: 'Path to your logo from website root. We recommend that you use a square image that is at least 256x256. (upload functionality coming soon)',
}, };
export const TEXTFIELD_PROPS_STREAM_KEY = {
extraPageContent: { apiPath: API_STREAM_KEY,
apiPath: '/pagecontent', configPath: '',
placeholder: '',
label: 'Extra page content',
tip: 'Custom markup about yourself',
},
nsfw: {
apiPath: '/nsfw',
placeholder: '',
label: 'NSFW?',
tip: "Turn this ON if you plan to steam explicit or adult content. You may want to respectfully set this flag so that unexpecting eyes won't accidentally see it from the Directory.",
},
tags: {
apiPath: '/tags',
defaultValue: '',
maxLength: 24,
placeholder: 'Add a new tag',
label: '',
tip: '',
},
},
streamKey: {
apiPath: '/key',
defaultValue: 'abc123',
maxLength: TEXT_MAXLENGTH, maxLength: TEXT_MAXLENGTH,
placeholder: 'abc123', placeholder: 'abc123',
label: 'Stream Key', label: 'Stream Key',
tip: 'Secret stream key', tip: 'Secret stream key',
required: true, required: true,
}, };
export const TEXTFIELD_PROPS_FFMPEG = {
ffmpegPath: { apiPath: API_FFMPEG,
apiPath: '/ffmpegpath', configPath: '',
defaultValue: '',
maxLength: TEXT_MAXLENGTH, maxLength: TEXT_MAXLENGTH,
placeholder: '/usr/local/bin/ffmpeg', placeholder: '/usr/local/bin/ffmpeg',
label: 'FFmpeg Path', label: 'FFmpeg Path',
tip: 'Absolute file path of the FFMPEG application on your server', tip: 'Absolute file path of the FFMPEG application on your server',
required: true, required: true,
}, };
export const TEXTFIELD_PROPS_WEB_PORT = {
webServerPort: { apiPath: API_WEB_PORT,
apiPath: '/webserverport', configPath: '',
defaultValue: '8080',
maxLength: 6, maxLength: 6,
placeholder: '8080', placeholder: '8080',
label: 'Owncast Server port', label: 'Owncast Server port',
tip: 'What port are you serving Owncast from? Default is :8080', tip: 'What port are you serving Owncast from? Default is :8080',
required: true, required: true,
}, };
rtmpServerPort: { export const TEXTFIELD_PROPS_RTMP_PORT = {
apiPath: '/rtmpserverport', apiPath: API_RTMP_PORT,
defaultValue: '1935', configPath: '',
maxLength: 6, maxLength: 6,
placeholder: '1935', placeholder: '1935',
label: 'RTMP port', label: 'RTMP port',
tip: 'What port are you receiving RTMP?', tip: 'What port are you receiving RTMP?',
required: true, required: true,
}, };
export const TEXTFIELD_PROPS_INSTANCE_URL = {
s3: { apiPath: API_INSTANCE_URL,
// tbd configPath: 'yp',
},
// YP options
yp: {
instanceUrl: {
apiPath: '/serverurl',
defaultValue: 'https://owncast.mysite.com',
maxLength: 255, maxLength: 255,
placeholder: 'url', placeholder: 'https://owncast.mysite.com',
label: 'Instance URL', label: 'Instance URL',
tip: 'Please provide the url to your Owncast site if you enable this Directory setting.', tip: 'Please provide the url to your Owncast site if you enable this Directory setting.',
}, };
enabled: { // MISC FIELDS
apiPath: '/directoryenabled', export const FIELD_PROPS_TAGS = {
defaultValue: false, apiPath: API_TAGS,
maxLength: 0, configPath: 'instanceDetails',
maxLength: 24,
placeholder: 'Add a new tag',
required: true,
label: '',
tip: '',
};
export const FIELD_PROPS_CUSTOM_CONTENT = {
apiPath: API_CUSTOM_CONTENT,
configPath: 'instanceDetails',
placeholder: '', placeholder: '',
label: 'Extra page content',
tip: 'Custom markup about yourself',
};
export const FIELD_PROPS_NSFW = {
apiPath: API_NSFW_SWITCH,
configPath: 'instanceDetails',
label: 'NSFW?',
tip: "Turn this ON if you plan to steam explicit or adult content. You may want to respectfully set this flag so that unexpecting eyes won't accidentally see it from the Directory.",
};
export const FIELD_PROPS_YP = {
apiPath: API_YP_SWITCH,
configPath: 'yp',
label: 'Display in the Owncast Directory?', label: 'Display in the Owncast Directory?',
tip: 'Turn this ON if you want to show up in the Owncast directory at https://directory.owncast.online.', tip: 'Turn this ON if you want to show up in the Owncast directory at https://directory.owncast.online.',
} };
},
videoSettings: {
// number slider
numberOfPlaylistItems: {
apiPath: '/webserverport', // tbd
defaultValue: 4,
maxLength: 3,
placeholder: '4',
label: 'Segment Length',
tip: '',
required: true,
minValue: 1,
maxValue: 10,
},
// number slider
segmentLengthSeconds: {
apiPath: '/webserverport', // tbd
defaultValue: 5,
maxLength: 3,
placeholder: '5',
label: 'Number of segments',
tip: '',
required: true,
minValue: 1,
maxValue: 10,
},
}
}
export const ENCODER_PRESETS = [ export const ENCODER_PRESETS = [
'fast', 'fast',

View File

@ -1,15 +1,16 @@
// Note: references to "yp" in the app are likely related to Owncast Directory // Note: references to "yp" in the app are likely related to Owncast Directory
import React, { useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import { Typography, Form } from 'antd'; import { Typography } from 'antd';
import ToggleSwitch from './form-toggleswitch'; import ToggleSwitch from './form-toggleswitch';
import { ServerStatusContext } from '../../../utils/server-status-context'; import { ServerStatusContext } from '../../../utils/server-status-context';
import { FIELD_PROPS_NSFW, FIELD_PROPS_YP } from './constants';
const { Title } = Typography; const { Title } = Typography;
export default function EditYPDetails() { export default function EditYPDetails() {
const [form] = Form.useForm(); const [formDataValues, setFormDataValues] = useState(null);
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig } = serverStatusData || {}; const { serverConfig } = serverStatusData || {};
@ -18,23 +19,19 @@ export default function EditYPDetails() {
const { nsfw } = instanceDetails; const { nsfw } = instanceDetails;
const { enabled, instanceUrl } = yp; const { enabled, instanceUrl } = yp;
const initialValues = {
useEffect(() => {
setFormDataValues({
...yp, ...yp,
enabled, enabled,
nsfw, nsfw,
}; });
}, [yp, instanceDetails]);
const hasInstanceUrl = instanceUrl !== ''; const hasInstanceUrl = instanceUrl !== '';
if (!formDataValues) {
useEffect(() => { return null;
form.setFieldsValue(initialValues); }
}, [yp]);
const extraProps = {
initialValues,
disabled: !hasInstanceUrl,
};
return ( return (
<div className="config-directory-details-form"> <div className="config-directory-details-form">
<Title level={3}>Owncast Directory Settings</Title> <Title level={3}>Owncast Directory Settings</Title>
@ -44,13 +41,18 @@ export default function EditYPDetails() {
<p style={{ backgroundColor: 'black', fontSize: '.75rem', padding: '5px' }}><em>NOTE: You will need to have a URL specified in the <code>Instance URL</code> field to be able to use this.</em></p> <p style={{ backgroundColor: 'black', fontSize: '.75rem', padding: '5px' }}><em>NOTE: You will need to have a URL specified in the <code>Instance URL</code> field to be able to use this.</em></p>
<div className="config-yp-container"> <div className="config-yp-container">
<Form <ToggleSwitch
form={form} fieldName="enabled"
layout="vertical" {...FIELD_PROPS_YP}
> checked={formDataValues.enabled}
<ToggleSwitch fieldName="enabled" configPath="yp" {...extraProps}/> disabled={!hasInstanceUrl}
<ToggleSwitch fieldName="nsfw" configPath="instanceDetails" {...extraProps} /> />
</Form> <ToggleSwitch
fieldName="nsfw"
{...FIELD_PROPS_NSFW}
checked={formDataValues.nsfw}
disabled={!hasInstanceUrl}
/>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,100 @@
import React, { useState, useContext, useEffect } from 'react';
import TextField, { TEXTFIELD_TYPE_TEXTAREA, TEXTFIELD_TYPE_URL } from './form-textfield';
import { ServerStatusContext } from '../../../utils/server-status-context';
import { postConfigUpdateToAPI, TEXTFIELD_PROPS_USERNAME, TEXTFIELD_PROPS_INSTANCE_URL, TEXTFIELD_PROPS_SERVER_TITLE, TEXTFIELD_PROPS_STREAM_TITLE, TEXTFIELD_PROPS_SERVER_SUMMARY, TEXTFIELD_PROPS_LOGO, API_YP_SWITCH } from './constants';
import configStyles from '../../../styles/config-pages.module.scss';
export default function EditInstanceDetails() {
const [formDataValues, setFormDataValues] = useState(null);
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig } = serverStatusData || {};
const { instanceDetails, yp } = serverConfig;
useEffect(() => {
setFormDataValues({
...instanceDetails,
...yp,
});
}, [instanceDetails, yp]);
if (!formDataValues) {
return null;
}
// if instanceUrl is empty, we should also turn OFF the `enabled` field of directory.
const handleSubmitInstanceUrl = () => {
if (formDataValues.instanceUrl === '') {
if (yp.enabled === true) {
postConfigUpdateToAPI({
apiPath: API_YP_SWITCH,
data: { value: false },
});
}
}
}
const handleFieldChange = (fieldName: string, value: string) => {
setFormDataValues({
...formDataValues,
[fieldName]: value,
});
}
return (
<div className={configStyles.publicDetailsContainer}>
<div className={configStyles.textFieldsSection}>
<TextField
fieldName="instanceUrl"
{...TEXTFIELD_PROPS_INSTANCE_URL}
value={formDataValues.instanceUrl}
initialValue={yp.instanceUrl}
type={TEXTFIELD_TYPE_URL}
onChange={handleFieldChange}
onSubmit={handleSubmitInstanceUrl}
/>
<TextField
fieldName="title"
{...TEXTFIELD_PROPS_SERVER_TITLE}
value={formDataValues.title}
initialValue={instanceDetails.title}
onChange={handleFieldChange}
/>
<TextField
fieldName="streamTitle"
{...TEXTFIELD_PROPS_STREAM_TITLE}
value={formDataValues.streamTitle}
initialValue={instanceDetails.streamTitle}
onChange={handleFieldChange}
/>
<TextField
fieldName="name"
{...TEXTFIELD_PROPS_USERNAME}
value={formDataValues.name}
initialValue={instanceDetails.name}
onChange={handleFieldChange}
/>
<TextField
fieldName="summary"
{...TEXTFIELD_PROPS_SERVER_SUMMARY}
type={TEXTFIELD_TYPE_TEXTAREA}
value={formDataValues.summary}
initialValue={instanceDetails.summary}
onChange={handleFieldChange}
/>
<TextField
fieldName="logo"
{...TEXTFIELD_PROPS_LOGO}
value={formDataValues.logo}
initialValue={instanceDetails.logo}
onChange={handleFieldChange}
/>
</div>
</div>
);
}

View File

@ -0,0 +1,73 @@
import React, { useState, useContext, useEffect } from 'react';
import TextField, { TEXTFIELD_TYPE_NUMBER, TEXTFIELD_TYPE_PASSWORD } from './form-textfield';
import { ServerStatusContext } from '../../../utils/server-status-context';
import { TEXTFIELD_PROPS_FFMPEG, TEXTFIELD_PROPS_RTMP_PORT, TEXTFIELD_PROPS_STREAM_KEY, TEXTFIELD_PROPS_WEB_PORT, } from './constants';
import configStyles from '../../../styles/config-pages.module.scss';
export default function EditInstanceDetails() {
const [formDataValues, setFormDataValues] = useState(null);
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig } = serverStatusData || {};
const { streamKey, ffmpegPath, rtmpServerPort, webServerPort } = serverConfig;
useEffect(() => {
setFormDataValues({
streamKey, ffmpegPath, rtmpServerPort, webServerPort
});
}, [serverConfig]);
if (!formDataValues) {
return null;
}
const handleFieldChange = (fieldName: string, value: string) => {
setFormDataValues({
...formDataValues,
[fieldName]: value,
});
}
return (
<div className={configStyles.publicDetailsContainer}>
<div className={configStyles.textFieldsSection}>
<TextField
fieldName="streamKey"
{...TEXTFIELD_PROPS_STREAM_KEY}
value={formDataValues.streamKey}
initialValue={streamKey}
type={TEXTFIELD_TYPE_PASSWORD}
onChange={handleFieldChange}
/>
<TextField
fieldName="ffmpegPath"
{...TEXTFIELD_PROPS_FFMPEG}
value={formDataValues.ffmpegPath}
initialValue={ffmpegPath}
onChange={handleFieldChange}
/>
<TextField
fieldName="webServerPort"
{...TEXTFIELD_PROPS_WEB_PORT}
value={formDataValues.webServerPort}
initialValue={webServerPort}
type={TEXTFIELD_TYPE_NUMBER}
onChange={handleFieldChange}
/>
<TextField
fieldName="rtmpServerPort"
{...TEXTFIELD_PROPS_RTMP_PORT}
value={formDataValues.rtmpServerPort}
initialValue={rtmpServerPort}
type={TEXTFIELD_TYPE_NUMBER}
onChange={handleFieldChange}
/>
</div>
</div>
);
}

View File

@ -3,7 +3,7 @@ import React, { useContext, useState, useEffect } from 'react';
import { Typography, Tag, Input } from 'antd'; import { Typography, Tag, Input } from 'antd';
import { ServerStatusContext } from '../../../utils/server-status-context'; import { ServerStatusContext } from '../../../utils/server-status-context';
import { TEXTFIELD_DEFAULTS, RESET_TIMEOUT, SUCCESS_STATES, postConfigUpdateToAPI } from './constants'; import { FIELD_PROPS_TAGS, RESET_TIMEOUT, SUCCESS_STATES, postConfigUpdateToAPI } from './constants';
const { Title } = Typography; const { Title } = Typography;
@ -17,14 +17,12 @@ export default function EditInstanceTags() {
const { instanceDetails } = serverConfig; const { instanceDetails } = serverConfig;
const { tags = [] } = instanceDetails; const { tags = [] } = instanceDetails;
const configPath = 'instanceDetails';
const { const {
apiPath, apiPath,
maxLength, maxLength,
placeholder, placeholder,
} = TEXTFIELD_DEFAULTS[configPath].tags || {}; configPath,
} = FIELD_PROPS_TAGS;
let resetTimer = null; let resetTimer = null;

View File

@ -1,9 +1,9 @@
import React, { useState, useContext } from 'react'; import React, { useState, useContext } from 'react';
import { Button, Form, Input, InputNumber } from 'antd'; import { Button, Input, InputNumber } from 'antd';
import { FormItemProps } from 'antd/es/form'; import { FormItemProps } from 'antd/es/form';
import { TEXTFIELD_DEFAULTS, TEXT_MAXLENGTH, RESET_TIMEOUT, postConfigUpdateToAPI } from './constants'; import { RESET_TIMEOUT, postConfigUpdateToAPI } from './constants';
import { TextFieldProps } from '../../../types/config-section'; import { TextFieldProps } from '../../../types/config-section';
import { ServerStatusContext } from '../../../utils/server-status-context'; import { ServerStatusContext } from '../../../utils/server-status-context';
@ -22,37 +22,29 @@ export default function TextField(props: TextFieldProps) {
const [hasChanged, setHasChanged] = useState(false); const [hasChanged, setHasChanged] = useState(false);
const [fieldValueForSubmit, setFieldValueForSubmit] = useState(''); const [fieldValueForSubmit, setFieldValueForSubmit] = useState('');
let resetTimer = null;
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { setFieldInConfigState } = serverStatusData || {}; const { setFieldInConfigState } = serverStatusData || {};
let resetTimer = null;
const { const {
apiPath,
configPath = '', configPath = '',
disabled = false, disabled = false,
fieldName, fieldName,
handleResetValue = () => {}, initialValue,
initialValues = {}, label,
placeholder, maxLength,
onSubmit,
onBlur, onBlur,
onChange, onChange,
onSubmit,
placeholder,
required,
tip,
type, type,
value,
} = props; } = props;
// Keep track of what the initial value is
// Note: we're not using `initialValue` as a prop, because we expect this component to be controlled by a parent Ant <Form> which is doing a form.setFieldsValue() upstream.
const initialValue = initialValues[fieldName] || '';
// Get other static info we know about this field.
const defaultDetails = TEXTFIELD_DEFAULTS[configPath] || TEXTFIELD_DEFAULTS;
const {
apiPath = '',
maxLength = TEXT_MAXLENGTH,
label = '',
tip = '',
required = false,
} = defaultDetails[fieldName] || {};
// Clear out any validation states and messaging // Clear out any validation states and messaging
const resetStates = () => { const resetStates = () => {
@ -79,17 +71,19 @@ export default function TextField(props: TextFieldProps) {
} }
// if an extra onChange handler was sent in as a prop, let's run that too. // if an extra onChange handler was sent in as a prop, let's run that too.
if (onChange) { if (onChange) {
onChange(); onChange(fieldName, val);
} }
}; };
// if you blur a required field with an empty value, restore its original value // if you blur a required field with an empty value, restore its original value in state (parent's state), if an onChange from parent is available.
const handleBlur = e => { const handleBlur = e => {
if (!onChange) {
return;
}
const val = e.target.value; const val = e.target.value;
if (required && val === '') { if (required && val === '') {
handleResetValue(fieldName); onChange(fieldName, initialValue);
} }
// if an extra onBlur handler was sent in as a prop, let's run that too. // if an extra onBlur handler was sent in as a prop, let's run that too.
if (onBlur) { if (onBlur) {
onBlur(); onBlur();
@ -153,30 +147,29 @@ export default function TextField(props: TextFieldProps) {
}; };
} }
const fieldId = `field-${fieldName}`;
return ( return (
<div className={`textfield-container type-${type}`}> <div className={`textfield-container type-${type}`}>
<div className="textfield-label">{label}</div> { required ? <span className="required-label">*</span> : null }
<label htmlFor={fieldId} className="textfield-label">{label}</label>
<div className="textfield"> <div className="textfield">
<Form.Item
name={fieldName}
hasFeedback
validateStatus={submitStatus}
help={submitStatusMessage}
required={required}
>
<Field <Field
className={`field field-${fieldName}`} id={fieldId}
className={`field ${fieldId}`}
{...fieldProps}
allowClear allowClear
placeholder={placeholder} placeholder={placeholder}
maxLength={maxLength} maxLength={maxLength}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
disabled={disabled} disabled={disabled}
{...fieldProps} value={value}
/> />
</Form.Item>
</div> </div>
<InfoTip tip={tip} /> <InfoTip tip={tip} />
{submitStatus}
{submitStatusMessage}
{ hasChanged ? <Button type="primary" size="small" className="submit-button" onClick={handleSubmit}>Update</Button> : null } { hasChanged ? <Button type="primary" size="small" className="submit-button" onClick={handleSubmit}>Update</Button> : null }

View File

@ -1,18 +1,13 @@
import React, { useState, useContext } from 'react'; import React, { useState, useContext } from 'react';
import { Form, Switch } from 'antd'; import { Switch } from 'antd';
import { FormItemProps } from 'antd/es/form'; import { FormItemProps } from 'antd/es/form';
import { TEXTFIELD_DEFAULTS, RESET_TIMEOUT, SUCCESS_STATES, postConfigUpdateToAPI } from './constants'; import { RESET_TIMEOUT, SUCCESS_STATES, postConfigUpdateToAPI } from './constants';
import { ToggleSwitchProps } from '../../../types/config-section'; import { ToggleSwitchProps } from '../../../types/config-section';
import { ServerStatusContext } from '../../../utils/server-status-context'; import { ServerStatusContext } from '../../../utils/server-status-context';
import InfoTip from '../info-tip'; import InfoTip from '../info-tip';
export const TEXTFIELD_TYPE_TEXT = 'default';
export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password
export const TEXTFIELD_TYPE_NUMBER = 'numeric';
export const TEXTFIELD_TYPE_TEXTAREA = 'textarea';
export default function ToggleSwitch(props: ToggleSwitchProps) { export default function ToggleSwitch(props: ToggleSwitchProps) {
const [submitStatus, setSubmitStatus] = useState<FormItemProps['validateStatus']>(''); const [submitStatus, setSubmitStatus] = useState<FormItemProps['validateStatus']>('');
@ -24,35 +19,28 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
const { setFieldInConfigState } = serverStatusData || {}; const { setFieldInConfigState } = serverStatusData || {};
const { const {
fieldName, apiPath,
initialValues = {}, checked,
configPath = '', configPath = '',
disabled = false, disabled = false,
fieldName,
label,
tip,
} = props; } = props;
const initialValue = initialValues[fieldName] || false;
const defaultDetails = TEXTFIELD_DEFAULTS[configPath] || TEXTFIELD_DEFAULTS;
const {
apiPath = '',
label = '',
tip = '',
} = defaultDetails[fieldName] || {};
const resetStates = () => { const resetStates = () => {
setSubmitStatus(''); setSubmitStatus('');
clearTimeout(resetTimer); clearTimeout(resetTimer);
resetTimer = null; resetTimer = null;
} }
const handleChange = async checked => { const handleChange = async isChecked => {
setSubmitStatus('validating'); setSubmitStatus('validating');
await postConfigUpdateToAPI({ await postConfigUpdateToAPI({
apiPath, apiPath,
data: { value: checked }, data: { value: isChecked },
onSuccess: () => { onSuccess: () => {
setFieldInConfigState({ fieldName, value: checked, path: configPath }); setFieldInConfigState({ fieldName, value: isChecked, path: configPath });
setSubmitStatus('success'); setSubmitStatus('success');
}, },
onError: (message: string) => { onError: (message: string) => {
@ -71,23 +59,17 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
return ( return (
<div className="toggleswitch-container"> <div className="toggleswitch-container">
<div className="toggleswitch"> <div className="toggleswitch">
<Form.Item
name={fieldName}
validateStatus={submitStatus}
>
<Switch <Switch
className={`switch field-${fieldName}`} className={`switch field-${fieldName}`}
loading={submitStatus === 'validating'} loading={submitStatus === 'validating'}
onChange={handleChange} onChange={handleChange}
checked={initialValue} defaultChecked={checked}
checkedChildren="ON" checkedChildren="ON"
unCheckedChildren="OFF" unCheckedChildren="OFF"
disabled={disabled} disabled={disabled}
/> />
</Form.Item>
<span className="label">{label} <InfoTip tip={tip} /></span> <span className="label">{label} <InfoTip tip={tip} /></span>
{submitStatus}
</div> </div>
<div className={`status-message ${submitStatus || ''}`}> <div className={`status-message ${submitStatus || ''}`}>
{newStatusIcon} {newStatusMessage} {submitStatusMessage} {newStatusIcon} {newStatusMessage} {submitStatusMessage}

View File

@ -5,7 +5,7 @@ import dynamic from 'next/dynamic';
import MarkdownIt from 'markdown-it'; import MarkdownIt from 'markdown-it';
import { ServerStatusContext } from '../utils/server-status-context'; import { ServerStatusContext } from '../utils/server-status-context';
import { TEXTFIELD_DEFAULTS, postConfigUpdateToAPI, RESET_TIMEOUT, SUCCESS_STATES} from './components/config/constants'; import { postConfigUpdateToAPI, RESET_TIMEOUT, SUCCESS_STATES, API_CUSTOM_CONTENT} from './components/config/constants';
import 'react-markdown-editor-lite/lib/index.css'; import 'react-markdown-editor-lite/lib/index.css';
@ -29,7 +29,6 @@ export default function PageContentEditor() {
const { instanceDetails } = serverConfig; const { instanceDetails } = serverConfig;
const { extraPageContent: initialContent } = instanceDetails; const { extraPageContent: initialContent } = instanceDetails;
const { apiPath } = TEXTFIELD_DEFAULTS.instanceDetails.extraPageContent;
let resetTimer = null; let resetTimer = null;
@ -54,10 +53,10 @@ export default function PageContentEditor() {
async function handleSave() { async function handleSave() {
setSubmitStatus('validating'); setSubmitStatus('validating');
await postConfigUpdateToAPI({ await postConfigUpdateToAPI({
apiPath, apiPath: API_CUSTOM_CONTENT,
data: { value: content }, data: { value: content },
onSuccess: () => { onSuccess: () => {
setFieldInConfigState({ fieldName: 'extraPageContent', value: content, path: apiPath }); setFieldInConfigState({ fieldName: 'extraPageContent', value: content, path: 'instanceDetails' });
setSubmitStatus('success'); setSubmitStatus('success');
}, },
onError: (message: string) => { onError: (message: string) => {

View File

@ -1,76 +1,21 @@
import React, { useContext, useEffect } from 'react'; import React from 'react';
import { Typography, Form } from 'antd'; import { Typography } from 'antd';
import Link from 'next/link'; import Link from 'next/link';
import TextField, { TEXTFIELD_TYPE_TEXTAREA, TEXTFIELD_TYPE_URL } from './components/config/form-textfield';
import { ServerStatusContext } from '../utils/server-status-context';
import { TEXTFIELD_DEFAULTS, postConfigUpdateToAPI } from './components/config/constants';
import configStyles from '../styles/config-pages.module.scss'; import configStyles from '../styles/config-pages.module.scss';
import EditInstanceDetails from './components/config/edit-instance-details';
const { Title } = Typography; const { Title } = Typography;
export default function PublicFacingDetails() { export default function PublicFacingDetails() {
const [form] = Form.useForm();
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig } = serverStatusData || {};
const { instanceDetails, yp } = serverConfig;
const initialValues = {
...instanceDetails,
...yp,
};
useEffect(() => {
form.setFieldsValue(initialValues);
}, [instanceDetails]);
// if instanceUrl is empty, we should also turn OFF the `enabled` field of directory.
const handleSubmitInstanceUrl = () => {
if (form.getFieldValue('instanceUrl') === '') {
if (yp.enabled === true) {
const { apiPath } = TEXTFIELD_DEFAULTS.yp.enabled;
postConfigUpdateToAPI({
apiPath,
data: { value: false },
});
}
}
}
const extraProps = {
initialValues,
configPath: 'instanceDetails',
};
return ( return (
<> <>
<Title level={2}>Edit your public facing instance details</Title> <Title level={2}>Edit your public facing instance details</Title>
<div className={configStyles.publicDetailsContainer}> <div className={configStyles.publicDetailsContainer}>
<div className={configStyles.textFieldsSection}> <div className={configStyles.textFieldsSection}>
<Form <EditInstanceDetails />
form={form}
layout="vertical"
>
<TextField
fieldName="instanceUrl"
{...extraProps}
configPath="yp"
type={TEXTFIELD_TYPE_URL}
onSubmit={handleSubmitInstanceUrl}
/>
<TextField fieldName="title" {...extraProps} />
<TextField fieldName="streamTitle" {...extraProps} />
<TextField fieldName="name" {...extraProps} />
<TextField fieldName="summary" type={TEXTFIELD_TYPE_TEXTAREA} {...extraProps} />
<TextField fieldName="logo" {...extraProps} />
</Form>
<Link href="/admin/config-page-content"> <Link href="/admin/config-page-content">
<a>Edit your extra page content here.</a> <a>Edit your extra page content here.</a>

View File

@ -1,57 +1,17 @@
import React, { useContext, useEffect } from 'react'; import React from 'react';
import { Typography, Form } from 'antd'; import { Typography } from 'antd';
import EditServerDetails from './components/config/edit-server-details';
import TextField, { TEXTFIELD_TYPE_NUMBER, TEXTFIELD_TYPE_PASSWORD } from './components/config/form-textfield';
import { ServerStatusContext } from '../utils/server-status-context';
import { TEXTFIELD_DEFAULTS } from './components/config/constants';
const { Title } = Typography; const { Title } = Typography;
export default function ConfigServerDetails() { export default function ConfigServerDetails() {
const [form] = Form.useForm();
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig } = serverStatusData || {};
const { ffmpegPath, streamKey, webServerPort, rtmpServerPort } = serverConfig;
const initialValues = {
ffmpegPath,
streamKey,
webServerPort,
rtmpServerPort,
};
useEffect(() => {
form.setFieldsValue(initialValues);
}, [serverStatusData]);
const handleResetValue = (fieldName: string) => {
const defaultValue = TEXTFIELD_DEFAULTS[fieldName] && TEXTFIELD_DEFAULTS[fieldName].defaultValue || '';
form.setFieldsValue({ [fieldName]: initialValues[fieldName] || defaultValue });
};
const extraProps = {
handleResetValue,
initialValues,
configPath: '',
};
return ( return (
<div className="config-server-details-form"> <div className="config-server-details-form">
<Title level={2}>Edit your Server&apos;s details</Title> <Title level={2}>Edit your Server&apos;s details</Title>
<div className="config-public-details-container"> <div className="config-server-details-container">
<Form <EditServerDetails />
form={form}
layout="vertical"
>
<TextField fieldName="streamKey" type={TEXTFIELD_TYPE_PASSWORD} {...extraProps} />
<TextField fieldName="ffmpegPath" {...extraProps} />
<TextField fieldName="webServerPort" type={TEXTFIELD_TYPE_NUMBER} {...extraProps} />
<TextField fieldName="rtmpServerPort" type={TEXTFIELD_TYPE_NUMBER} {...extraProps} />
</Form>
</div> </div>
</div> </div>
); );

View File

@ -16,7 +16,6 @@ import StatisticItem from "./components/statistic"
import LogTable from "./components/log-table"; import LogTable from "./components/log-table";
import Offline from './offline-notice'; import Offline from './offline-notice';
import TextField, { TEXTFIELD_TYPE_TEXTAREA, TEXTFIELD_TYPE_URL } from './components/config/form-textfield'; import TextField, { TEXTFIELD_TYPE_TEXTAREA, TEXTFIELD_TYPE_URL } from './components/config/form-textfield';
import { TEXTFIELD_DEFAULTS, postConfigUpdateToAPI } from './components/config/constants';
import { import {
LOGS_WARN, LOGS_WARN,

View File

@ -88,6 +88,7 @@ const ServerStatusProvider = ({ children }) => {
...config, ...config,
[fieldName]: value, [fieldName]: value,
}; };
console.log({updatedConfig, fieldName, value, path})
setConfig(updatedConfig); setConfig(updatedConfig);
}; };