Merge branch 'css-overhaul' into gw/20201226-admin-formfields

This commit is contained in:
gingervitis
2021-02-01 00:36:47 -08:00
19 changed files with 763 additions and 392 deletions

View File

@@ -184,3 +184,69 @@ export const DEFAULT_SOCIAL_HANDLE: SocialHandle = {
}; };
export const OTHER_SOCIAL_HANDLE_OPTION = 'OTHER_SOCIAL_HANDLE_OPTION'; export const OTHER_SOCIAL_HANDLE_OPTION = 'OTHER_SOCIAL_HANDLE_OPTION';
export const TEXTFIELD_PROPS_S3_COMMON = {
maxLength: 255,
}
// export const FIELD_PROPS_CUSTOM_CONTENT = {
// apiPath: API_CUSTOM_CONTENT,
// configPath: 'instanceDetails',
// placeholder: '',
// label: 'Extra page content',
// tip: 'Custom markup about yourself',
// };
export const S3_TEXT_FIELDS_INFO = {
accessKey: {
fieldName: 'accessKey',
label: 'Access Key',
maxLength: 255,
placeholder: 'access key 123',
tip: '',
},
acl: {
fieldName: 'acl',
label: 'ACL',
maxLength: 255,
placeholder: 'acl thing',
tip: '',
},
bucket: {
fieldName: 'bucket',
label: 'Bucket',
maxLength: 255,
placeholder: 'bucket 123',
tip: '',
},
endpoint: {
fieldName: 'endpoint',
label: 'Endpoint',
maxLength: 255,
placeholder: 'endpoint 123',
tip: 'This field has a some info',
},
region: {
fieldName: 'region',
label: 'Region',
maxLength: 255,
placeholder: 'region 123',
tip: '',
},
secret: {
fieldName: 'secret',
label: 'Secret key',
maxLength: 255,
placeholder: 'secret key 123',
tip: '',
},
servingEndpoint: {
fieldName: 'servingEndpoint',
label: 'Serving Endpoint',
maxLength: 255,
placeholder: 'servingEndpoint 123',
tip: '',
},
};

View File

@@ -16,7 +16,6 @@ import {
API_YP_SWITCH, API_YP_SWITCH,
} from './constants'; } from './constants';
import configStyles from '../../../styles/config-pages.module.scss';
import { UpdateArgs } from '../../../types/config-section'; import { UpdateArgs } from '../../../types/config-section';
export default function EditInstanceDetails() { export default function EditInstanceDetails() {
@@ -57,8 +56,8 @@ export default function EditInstanceDetails() {
}; };
return ( return (
<div className={configStyles.publicDetailsContainer}> <div className={`publicDetailsContainer`}>
<div className={configStyles.textFieldsSection}> <div className={`textFieldsSection`}>
<TextFieldWithSubmit <TextFieldWithSubmit
fieldName="instanceUrl" fieldName="instanceUrl"
{...TEXTFIELD_PROPS_INSTANCE_URL} {...TEXTFIELD_PROPS_INSTANCE_URL}

View File

@@ -13,7 +13,6 @@ import {
TEXTFIELD_PROPS_WEB_PORT, TEXTFIELD_PROPS_WEB_PORT,
} from './constants'; } from './constants';
import configStyles from '../../../styles/config-pages.module.scss';
import { UpdateArgs } from '../../../types/config-section'; import { UpdateArgs } from '../../../types/config-section';
export default function EditInstanceDetails() { export default function EditInstanceDetails() {
@@ -64,50 +63,52 @@ export default function EditInstanceDetails() {
} }
return ( return (
<div className={configStyles.publicDetailsContainer}> <div className="edit-public-details-container">
<div className={configStyles.textFieldsSection}> <div className="field-container field-streamkey-container">
<TextFieldWithSubmit <div className="left-side">
fieldName="streamKey" <TextFieldWithSubmit
{...TEXTFIELD_PROPS_STREAM_KEY} fieldName="streamKey"
value={formDataValues.streamKey} {...TEXTFIELD_PROPS_STREAM_KEY}
initialValue={streamKey} value={formDataValues.streamKey}
type={TEXTFIELD_TYPE_PASSWORD} initialValue={streamKey}
onChange={handleFieldChange} type={TEXTFIELD_TYPE_PASSWORD}
/> onChange={handleFieldChange}
<div> />
<span style={{ fontSize: '0.75em', color: '#ff7777', marginRight: '0.5em' }}> <div className="streamkey-actions">
Save this key somewhere safe, you will need it to stream or login to the admin <Tooltip className="copy-tooltip" title="Copied!" trigger="" visible={copyIsVisible}>
dashboard! <Button icon={<CopyOutlined />} size="small" onClick={copyStreamKey} />
</span> </Tooltip>
<Tooltip className="copy-tooltip" title="Copied!" trigger="" visible={copyIsVisible}> <Button icon={<RedoOutlined />} size="small" onClick={generateStreamKey} />
<Button type="primary" icon={<CopyOutlined />} size="small" onClick={copyStreamKey} /> </div>
</Tooltip> </div>
<Button type="primary" icon={<RedoOutlined />} size="small" onClick={generateStreamKey} /> <div className="streamkey-notice">
Save this key somewhere safe, you will need it to stream or login to the admin
dashboard!
</div> </div>
<TextFieldWithSubmit
fieldName="ffmpegPath"
{...TEXTFIELD_PROPS_FFMPEG}
value={formDataValues.ffmpegPath}
initialValue={ffmpegPath}
onChange={handleFieldChange}
/>
<TextFieldWithSubmit
fieldName="webServerPort"
{...TEXTFIELD_PROPS_WEB_PORT}
value={formDataValues.webServerPort}
initialValue={webServerPort}
type={TEXTFIELD_TYPE_NUMBER}
onChange={handleFieldChange}
/>
<TextFieldWithSubmit
fieldName="rtmpServerPort"
{...TEXTFIELD_PROPS_RTMP_PORT}
value={formDataValues.rtmpServerPort}
initialValue={rtmpServerPort}
type={TEXTFIELD_TYPE_NUMBER}
onChange={handleFieldChange}
/>
</div> </div>
<TextFieldWithSubmit
fieldName="ffmpegPath"
{...TEXTFIELD_PROPS_FFMPEG}
value={formDataValues.ffmpegPath}
initialValue={ffmpegPath}
onChange={handleFieldChange}
/>
<TextFieldWithSubmit
fieldName="webServerPort"
{...TEXTFIELD_PROPS_WEB_PORT}
value={formDataValues.webServerPort}
initialValue={webServerPort}
type={TEXTFIELD_TYPE_NUMBER}
onChange={handleFieldChange}
/>
<TextFieldWithSubmit
fieldName="rtmpServerPort"
{...TEXTFIELD_PROPS_RTMP_PORT}
value={formDataValues.rtmpServerPort}
initialValue={rtmpServerPort}
type={TEXTFIELD_TYPE_NUMBER}
onChange={handleFieldChange}
/>
</div> </div>
); );
} }

View File

@@ -9,14 +9,14 @@ import {
API_SOCIAL_HANDLES, API_SOCIAL_HANDLES,
postConfigUpdateToAPI, postConfigUpdateToAPI,
RESET_TIMEOUT, RESET_TIMEOUT,
SUCCESS_STATES,
DEFAULT_SOCIAL_HANDLE, DEFAULT_SOCIAL_HANDLE,
OTHER_SOCIAL_HANDLE_OPTION, OTHER_SOCIAL_HANDLE_OPTION,
} from './constants'; } from './constants';
import { SocialHandle } from '../../../types/config-section'; import { SocialHandle, UpdateArgs } from '../../../types/config-section';
import { isValidUrl } from '../../../utils/urls'; import { isValidUrl } from '../../../utils/urls';
import TextField from './form-textfield';
import configStyles from '../../../styles/config-pages.module.scss'; import { createInputStatus, STATUS_ERROR, STATUS_SUCCESS } from '../../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator';
const { Title } = Typography; const { Title } = Typography;
@@ -33,7 +33,6 @@ export default function EditSocialLinks() {
const [modalDataState, setModalDataState] = useState(DEFAULT_SOCIAL_HANDLE); const [modalDataState, setModalDataState] = useState(DEFAULT_SOCIAL_HANDLE);
const [submitStatus, setSubmitStatus] = useState(null); const [submitStatus, setSubmitStatus] = useState(null);
const [submitStatusMessage, setSubmitStatusMessage] = useState('');
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {}; const { serverConfig, setFieldInConfigState } = serverStatusData || {};
@@ -73,7 +72,6 @@ export default function EditSocialLinks() {
const resetStates = () => { const resetStates = () => {
setSubmitStatus(null); setSubmitStatus(null);
setSubmitStatusMessage('');
resetTimer = null; resetTimer = null;
clearTimeout(resetTimer); clearTimeout(resetTimer);
}; };
@@ -108,8 +106,8 @@ export default function EditSocialLinks() {
const { value } = event.target; const { value } = event.target;
updateModalState('platform', value); updateModalState('platform', value);
}; };
const handleUrlChange = event => {
const { value } = event.target; const handleUrlChange = ({ value }: UpdateArgs) => {
updateModalState('url', value); updateModalState('url', value);
}; };
@@ -129,13 +127,12 @@ export default function EditSocialLinks() {
setModalProcessing(false); setModalProcessing(false);
handleModalCancel(); handleModalCancel();
setSubmitStatus('success'); setSubmitStatus(createInputStatus(STATUS_SUCCESS));
setSubmitStatusMessage('Social Handles updated.');
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}, },
onError: (message: string) => { onError: (message: string) => {
setSubmitStatus('error'); setSubmitStatus(createInputStatus(STATUS_ERROR, `There was an error: ${message}`));
setSubmitStatusMessage(message);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}, },
}); });
@@ -223,24 +220,16 @@ export default function EditSocialLinks() {
}, },
]; ];
const { icon: newStatusIcon = null, message: newStatusMessage = '' } =
SUCCESS_STATES[submitStatus] || {};
const statusMessage = (
<div className={`status-message ${submitStatus || ''}`}>
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
</div>
);
const okButtonProps = { const okButtonProps = {
disabled: !isValidUrl(modalDataState.url), disabled: !isValidUrl(modalDataState.url),
}; };
return ( return (
<div className={configStyles.socialLinksEditor}> <div className="social-links-edit-container">
<Title level={2}>Social Links</Title> <Title level={2}>Social Links</Title>
<p>Add all your social media handles and links to your other profiles here.</p> <p>Add all your social media handles and links to your other profiles here.</p>
{statusMessage} <FormStatusIndicator status={submitStatus} />
<Table <Table
className="dataTable" className="dataTable"
@@ -275,14 +264,14 @@ export default function EditSocialLinks() {
</> </>
) : null} ) : null}
<br /> <br />
URL <TextField
<Input fieldName="social-url"
label="URL"
placeholder="Url to page" placeholder="Url to page"
defaultValue={modalDataState.url}
value={modalDataState.url} value={modalDataState.url}
onChange={handleUrlChange} onChange={handleUrlChange}
/> />
{statusMessage} <FormStatusIndicator status={submitStatus} />
</Modal> </Modal>
<br /> <br />
<Button <Button

View File

@@ -0,0 +1,218 @@
import { Switch, Button, Collapse } from 'antd';
import classNames from 'classnames';
import React, { useContext, useState, useEffect } from 'react';
import { UpdateArgs } from '../../../types/config-section';
import { ServerStatusContext } from '../../../utils/server-status-context';
import {
postConfigUpdateToAPI,
API_S3_INFO,
RESET_TIMEOUT,
S3_TEXT_FIELDS_INFO,
} from './constants';
import {
createInputStatus,
StatusState,
STATUS_ERROR,
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../../../utils/input-statuses';
import TextField from './form-textfield';
import FormStatusIndicator from './form-status-indicator';
const { Panel } = Collapse;
// we could probably add more detailed checks here
// `currentValues` is what's currently in the global store and in the db
function checkSaveable(formValues: any, currentValues: any) {
const { endpoint, accessKey, secret, bucket, region, enabled, servingEndpoint, acl } = formValues;
// if fields are filled out and different from what's in store, then return true
if (enabled) {
if (!!endpoint && !!accessKey && !!secret && !!bucket && !!region) {
if (
endpoint !== currentValues.endpoint ||
accessKey !== currentValues.accessKey ||
secret !== currentValues.secret ||
region !== currentValues.region ||
(!!currentValues.servingEndpoint && servingEndpoint !== currentValues.servingEndpoint) ||
(!!currentValues.acl && acl !== currentValues.acl)
) {
return true;
}
}
} else if (enabled !== currentValues.enabled) {
return true;
}
return false;
}
export default function EditStorage() {
const [formDataValues, setFormDataValues] = useState(null);
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const [shouldDisplayForm, setShouldDisplayForm] = useState(false);
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { s3 } = serverConfig;
const {
accessKey = '',
acl = '',
bucket = '',
enabled = false,
endpoint = '',
region = '',
secret = '',
servingEndpoint = '',
} = s3;
useEffect(() => {
setFormDataValues({
accessKey,
acl,
bucket,
enabled,
endpoint,
region,
secret,
servingEndpoint,
});
setShouldDisplayForm(enabled);
}, [s3]);
if (!formDataValues) {
return null;
}
let resetTimer = null;
const resetStates = () => {
setSubmitStatus(null);
resetTimer = null;
clearTimeout(resetTimer);
};
// update individual values in state
const handleFieldChange = ({ fieldName, value }: UpdateArgs) => {
setFormDataValues({
...formDataValues,
[fieldName]: value,
});
};
// posts the whole state
const handleSave = async () => {
setSubmitStatus(createInputStatus(STATUS_PROCESSING));
const postValue = formDataValues;
await postConfigUpdateToAPI({
apiPath: API_S3_INFO,
data: { value: postValue },
onSuccess: () => {
setFieldInConfigState({ fieldName: 's3', value: postValue, path: '' });
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
},
onError: (message: string) => {
setSubmitStatus(createInputStatus(STATUS_ERROR, message));
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
},
});
};
// toggle switch.
const handleSwitchChange = (storageEnabled: boolean) => {
setShouldDisplayForm(storageEnabled);
handleFieldChange({ fieldName: 'enabled', value: storageEnabled });
// if current data in current store says s3 is enabled,
// we should save this state
// if (!storageEnabled && s3.enabled) {
// handleSave();
// }
};
const containerClass = classNames({
'edit-storage-container': true,
enabled: shouldDisplayForm,
});
const isSaveable = checkSaveable(formDataValues, s3);
return (
<div className={containerClass}>
<div className="enable-switch">
<Switch
checked={formDataValues.enabled}
defaultChecked={formDataValues.enabled}
onChange={handleSwitchChange}
checkedChildren="ON"
unCheckedChildren="OFF"
/>{' '}
Enabled
</div>
<div className="form-fields">
<div className="field-container">
<TextField
{...S3_TEXT_FIELDS_INFO.endpoint}
value={formDataValues.endpoint}
onChange={handleFieldChange}
/>
</div>
<div className="field-container">
<TextField
{...S3_TEXT_FIELDS_INFO.accessKey}
value={formDataValues.accessKey}
onChange={handleFieldChange}
/>
</div>
<div className="field-container">
<TextField
{...S3_TEXT_FIELDS_INFO.secret}
value={formDataValues.secret}
onChange={handleFieldChange}
/>
</div>
<div className="field-container">
<TextField
{...S3_TEXT_FIELDS_INFO.bucket}
value={formDataValues.bucket}
onChange={handleFieldChange}
/>
</div>
<div className="field-container">
<TextField
{...S3_TEXT_FIELDS_INFO.region}
value={formDataValues.region}
onChange={handleFieldChange}
/>
</div>
<Collapse className="advanced-section">
<Panel header="Advanced Settings" key="1">
<div className="field-container">
<TextField
{...S3_TEXT_FIELDS_INFO.acl}
value={formDataValues.acl}
onChange={handleFieldChange}
/>
</div>
<div className="field-container">
<TextField
{...S3_TEXT_FIELDS_INFO.servingEndpoint}
value={formDataValues.servingEndpoint}
onChange={handleFieldChange}
/>
</div>
</Panel>
</Collapse>
</div>
<div className="button-container">
<Button type="primary" onClick={handleSave} disabled={!isSaveable}>
Save
</Button>
<FormStatusIndicator status={submitStatus} />
</div>
</div>
);
}

View File

@@ -19,7 +19,7 @@ const { Title } = Typography;
export default function EditInstanceTags() { export default function EditInstanceTags() {
const [newTagInput, setNewTagInput] = useState<string | number>(''); const [newTagInput, setNewTagInput] = useState<string | number>('');
const [fieldStatus, setFieldStatus] = useState<StatusState>(null); const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {}; const { serverConfig, setFieldInConfigState } = serverStatusData || {};
@@ -38,34 +38,34 @@ export default function EditInstanceTags() {
}, []); }, []);
const resetStates = () => { const resetStates = () => {
setFieldStatus(null); setSubmitStatus(null);
resetTimer = null; resetTimer = null;
clearTimeout(resetTimer); clearTimeout(resetTimer);
}; };
// posts all the tags at once as an array obj // posts all the tags at once as an array obj
const postUpdateToAPI = async (postValue: any) => { const postUpdateToAPI = async (postValue: any) => {
setFieldStatus(createInputStatus(STATUS_PROCESSING)); setSubmitStatus(createInputStatus(STATUS_PROCESSING));
await postConfigUpdateToAPI({ await postConfigUpdateToAPI({
apiPath, apiPath,
data: { value: postValue }, data: { value: postValue },
onSuccess: () => { onSuccess: () => {
setFieldInConfigState({ fieldName: 'tags', value: postValue, path: configPath }); setFieldInConfigState({ fieldName: 'tags', value: postValue, path: configPath });
setFieldStatus(createInputStatus(STATUS_SUCCESS, 'Tags updated.')); setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Tags updated.'));
setNewTagInput(''); setNewTagInput('');
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}, },
onError: (message: string) => { onError: (message: string) => {
setFieldStatus(createInputStatus(STATUS_ERROR, message)); setSubmitStatus(createInputStatus(STATUS_ERROR, message));
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}, },
}); });
}; };
const handleInputChange = ({ value }: UpdateArgs) => { const handleInputChange = ({ value }: UpdateArgs) => {
if (!fieldStatus) { if (!submitStatus) {
setFieldStatus(null); setSubmitStatus(null);
} }
setNewTagInput(value); setNewTagInput(value);
}; };
@@ -75,11 +75,11 @@ export default function EditInstanceTags() {
resetStates(); resetStates();
const newTag = newTagInput.trim(); const newTag = newTagInput.trim();
if (newTag === '') { if (newTag === '') {
setFieldStatus(createInputStatus(STATUS_WARNING, 'Please enter a tag')); setSubmitStatus(createInputStatus(STATUS_WARNING, 'Please enter a tag'));
return; return;
} }
if (tags.some(tag => tag.toLowerCase() === newTag.toLowerCase())) { if (tags.some(tag => tag.toLowerCase() === newTag.toLowerCase())) {
setFieldStatus(createInputStatus(STATUS_WARNING, 'This tag is already used!')); setSubmitStatus(createInputStatus(STATUS_WARNING, 'This tag is already used!'));
return; return;
} }
@@ -121,7 +121,7 @@ export default function EditInstanceTags() {
onPressEnter={handleSubmitNewTag} onPressEnter={handleSubmitNewTag}
maxLength={maxLength} maxLength={maxLength}
placeholder={placeholder} placeholder={placeholder}
status={fieldStatus} status={submitStatus}
/> />
</div> </div>
</div> </div>

View File

@@ -0,0 +1,22 @@
import React from 'react';
import classNames from 'classnames';
import { StatusState } from '../../../utils/input-statuses';
interface FormStatusIndicatorProps {
status: StatusState;
}
export default function FormStatusIndicator({ status }: FormStatusIndicatorProps) {
const { type, icon, message } = status || {};
const classes = classNames({
'status-container': true,
[`status-${type}`]: type,
empty: !message,
});
return (
<div className={classes}>
{icon ? <span className="status-icon">{icon}</span> : null}
{message ? <span className="status-message">{message}</span> : null}
</div>
);
}

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState, useContext } from 'react'; import React, { useEffect, useState, useContext } from 'react';
import { Button } from 'antd'; import { Button } from 'antd';
import classNames from 'classnames';
import { RESET_TIMEOUT, postConfigUpdateToAPI } from './constants'; import { RESET_TIMEOUT, postConfigUpdateToAPI } from './constants';
import { ServerStatusContext } from '../../../utils/server-status-context'; import { ServerStatusContext } from '../../../utils/server-status-context';
@@ -13,6 +13,7 @@ import {
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../../utils/input-statuses'; } from '../../../utils/input-statuses';
import { UpdateArgs } from '../../../types/config-section'; import { UpdateArgs } from '../../../types/config-section';
import FormStatusIndicator from './form-status-indicator';
export const TEXTFIELD_TYPE_TEXT = 'default'; export const TEXTFIELD_TYPE_TEXT = 'default';
export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password
@@ -27,7 +28,7 @@ interface TextFieldWithSubmitProps extends TextFieldProps {
} }
export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) { export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
const [fieldStatus, setFieldStatus] = useState<StatusState>(null); const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const [hasChanged, setHasChanged] = useState(false); const [hasChanged, setHasChanged] = useState(false);
@@ -43,11 +44,11 @@ export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
...textFieldProps // rest of props ...textFieldProps // rest of props
} = props; } = props;
const { fieldName, required, status, value, onChange, onSubmit } = textFieldProps; const { fieldName, required, tip, status, value, onChange, onSubmit } = textFieldProps;
// Clear out any validation states and messaging // Clear out any validation states and messaging
const resetStates = () => { const resetStates = () => {
setFieldStatus(null); setSubmitStatus(null);
setHasChanged(false); setHasChanged(false);
clearTimeout(resetTimer); clearTimeout(resetTimer);
resetTimer = null; resetTimer = null;
@@ -83,17 +84,17 @@ export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
// how to get current value of input // how to get current value of input
const handleSubmit = async () => { const handleSubmit = async () => {
if ((required && value !== '') || value !== initialValue) { if ((required && value !== '') || value !== initialValue) {
setFieldStatus(createInputStatus(STATUS_PROCESSING)); setSubmitStatus(createInputStatus(STATUS_PROCESSING));
await postConfigUpdateToAPI({ await postConfigUpdateToAPI({
apiPath, apiPath,
data: { value }, data: { value },
onSuccess: () => { onSuccess: () => {
setFieldInConfigState({ fieldName, value, path: configPath }); setFieldInConfigState({ fieldName, value, path: configPath });
setFieldStatus(createInputStatus(STATUS_SUCCESS)); setSubmitStatus(createInputStatus(STATUS_SUCCESS));
}, },
onError: (message: string) => { onError: (message: string) => {
setFieldStatus(createInputStatus(STATUS_ERROR, `There was an error: ${message}`)); setSubmitStatus(createInputStatus(STATUS_ERROR, `There was an error: ${message}`));
}, },
}); });
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
@@ -105,23 +106,38 @@ export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
} }
}; };
const textfieldContainerClass = classNames({
'textfield-with-submit-container': true,
submittable: hasChanged,
});
return ( return (
<div className="textfield-with-submit-container"> <div className={textfieldContainerClass}>
<TextField <div className="textfield-component">
{...textFieldProps} <TextField
status={status || fieldStatus} {...textFieldProps}
onSubmit={null} onSubmit={null}
onBlur={handleBlur} onBlur={handleBlur}
onChange={handleChange} onChange={handleChange}
/> />
</div>
{hasChanged ? ( <div className="textfield-container lower-container">
<div className="update-button-container"> <p className="label-spacer" />
<Button type="primary" size="small" className="submit-button" onClick={handleSubmit}> <div className="lower-content">
Update <div className="field-tip">{tip}</div>
</Button> <FormStatusIndicator status={status || submitStatus} />
<div className="update-button-container">
<Button
type="primary"
size="small"
className="submit-button"
onClick={handleSubmit}
disabled={!hasChanged}
>
Update
</Button>
</div>
</div> </div>
) : null} </div>
</div> </div>
); );
} }

View File

@@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { Input, InputNumber } from 'antd'; import { Input, InputNumber } from 'antd';
import { FieldUpdaterFunc } from '../../../types/config-section'; import { FieldUpdaterFunc } from '../../../types/config-section';
import InfoTip from '../info-tip'; // import InfoTip from '../info-tip';
import { StatusState } from '../../../utils/input-statuses'; import { StatusState } from '../../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator';
export const TEXTFIELD_TYPE_TEXT = 'default'; export const TEXTFIELD_TYPE_TEXT = 'default';
export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password
@@ -107,15 +109,20 @@ export default function TextField(props: TextFieldProps) {
const fieldId = `field-${fieldName}`; const fieldId = `field-${fieldName}`;
const { icon: statusIcon, message: statusMessage } = status || {}; const { type: statusType } = status || {};
const containerClass = classNames({
'textfield-container': true,
[`type-${type}`]: true,
required,
[`status-${statusType}`]: status,
});
return ( return (
<div className={`textfield-container type-${type}`}> <div className={containerClass}>
{label ? ( {label ? (
<div className="label-side"> <div className="label-side">
<label htmlFor={fieldId} className="textfield-label"> <label htmlFor={fieldId} className="textfield-label">
{required ? <span className="required-label">* </span> : null} {label}
{label}:
</label> </label>
</div> </div>
) : null} ) : null}
@@ -135,17 +142,13 @@ export default function TextField(props: TextFieldProps) {
disabled={disabled} disabled={disabled}
value={value} value={value}
/> />
</div> </div>
<div className="status-container"> <FormStatusIndicator status={status} />
{status ? <span className="status-icon">{statusIcon}</span> : null} <p className="field-tip">
{status ? <span className="status-message">{statusMessage}</span> : null} {tip}
</div> {/* <InfoTip tip={tip} /> */}
<p className="tip">
<InfoTip tip={tip} />
</p> </p>
</div> </div>
</div> </div>
); );
} }

View File

@@ -1,8 +1,15 @@
import React, { useState, useContext } from 'react'; import React, { useState, useContext } from 'react';
import { Switch } from 'antd'; import { Switch } from 'antd';
import { FormItemProps } from 'antd/es/form'; import {
createInputStatus,
StatusState,
STATUS_ERROR,
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator';
import { RESET_TIMEOUT, SUCCESS_STATES, postConfigUpdateToAPI } from './constants'; import { RESET_TIMEOUT, postConfigUpdateToAPI } from './constants';
import { ServerStatusContext } from '../../../utils/server-status-context'; import { ServerStatusContext } from '../../../utils/server-status-context';
import InfoTip from '../info-tip'; import InfoTip from '../info-tip';
@@ -19,8 +26,7 @@ interface ToggleSwitchProps {
} }
export default function ToggleSwitch(props: ToggleSwitchProps) { export default function ToggleSwitch(props: ToggleSwitchProps) {
const [submitStatus, setSubmitStatus] = useState<FormItemProps['validateStatus']>(''); const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const [submitStatusMessage, setSubmitStatusMessage] = useState('');
let resetTimer = null; let resetTimer = null;
@@ -30,37 +36,35 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
const { apiPath, checked, configPath = '', disabled = false, fieldName, label, tip } = props; const { apiPath, checked, configPath = '', disabled = false, fieldName, label, tip } = props;
const resetStates = () => { const resetStates = () => {
setSubmitStatus(''); setSubmitStatus(null);
clearTimeout(resetTimer); clearTimeout(resetTimer);
resetTimer = null; resetTimer = null;
}; };
const handleChange = async (isChecked: boolean) => { const handleChange = async (isChecked: boolean) => {
setSubmitStatus('validating'); setSubmitStatus(createInputStatus(STATUS_PROCESSING));
await postConfigUpdateToAPI({ await postConfigUpdateToAPI({
apiPath, apiPath,
data: { value: isChecked }, data: { value: isChecked },
onSuccess: () => { onSuccess: () => {
setFieldInConfigState({ fieldName, value: isChecked, path: configPath }); setFieldInConfigState({ fieldName, value: isChecked, path: configPath });
setSubmitStatus('success'); setSubmitStatus(createInputStatus(STATUS_SUCCESS));
}, },
onError: (message: string) => { onError: (message: string) => {
setSubmitStatus('error'); setSubmitStatus(createInputStatus(STATUS_ERROR, `There was an error: ${message}`));
setSubmitStatusMessage(`There was an error: ${message}`);
}, },
}); });
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}; };
const { icon: newStatusIcon = null, message: newStatusMessage = '' } = const loading = submitStatus !== null && submitStatus.type === STATUS_PROCESSING;
SUCCESS_STATES[submitStatus] || {};
return ( return (
<div className="toggleswitch-container"> <div className="toggleswitch-container">
<div className="toggleswitch"> <div className="toggleswitch">
<Switch <Switch
className={`switch field-${fieldName}`} className={`switch field-${fieldName}`}
loading={submitStatus === 'validating'} loading={loading}
onChange={handleChange} onChange={handleChange}
defaultChecked={checked} defaultChecked={checked}
checked={checked} checked={checked}
@@ -71,11 +75,8 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
<span className="label"> <span className="label">
{label} <InfoTip tip={tip} /> {label} <InfoTip tip={tip} />
</span> </span>
{submitStatus}
</div>
<div className={`status-message ${submitStatus || ''}`}>
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
</div> </div>
<FormStatusIndicator status={submitStatus} />
</div> </div>
); );
} }

View File

@@ -1,6 +1,5 @@
import React, { useState, useEffect, useContext } from 'react'; import React, { useState, useEffect, useContext } from 'react';
import { Typography, Button } from 'antd'; import { Typography, Button } from 'antd';
import { FormItemProps } from 'antd/lib/form';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import MarkdownIt from 'markdown-it'; import MarkdownIt from 'markdown-it';
@@ -8,11 +7,17 @@ import { ServerStatusContext } from '../utils/server-status-context';
import { import {
postConfigUpdateToAPI, postConfigUpdateToAPI,
RESET_TIMEOUT, RESET_TIMEOUT,
SUCCESS_STATES,
API_CUSTOM_CONTENT, API_CUSTOM_CONTENT,
} from './components/config/constants'; } from './components/config/constants';
import {
createInputStatus,
StatusState,
STATUS_ERROR,
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../utils/input-statuses';
import 'react-markdown-editor-lite/lib/index.css'; import 'react-markdown-editor-lite/lib/index.css';
import FormStatusIndicator from './components/config/form-status-indicator';
const { Title } = Typography; const { Title } = Typography;
@@ -24,8 +29,7 @@ const MdEditor = dynamic(() => import('react-markdown-editor-lite'), {
export default function PageContentEditor() { export default function PageContentEditor() {
const [content, setContent] = useState(''); const [content, setContent] = useState('');
const [submitStatus, setSubmitStatus] = useState<FormItemProps['validateStatus']>(''); const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const [submitStatusMessage, setSubmitStatusMessage] = useState('');
const [hasChanged, setHasChanged] = useState(false); const [hasChanged, setHasChanged] = useState(false);
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
@@ -47,7 +51,7 @@ export default function PageContentEditor() {
// Clear out any validation states and messaging // Clear out any validation states and messaging
const resetStates = () => { const resetStates = () => {
setSubmitStatus(''); setSubmitStatus(null);
setHasChanged(false); setHasChanged(false);
clearTimeout(resetTimer); clearTimeout(resetTimer);
resetTimer = null; resetTimer = null;
@@ -55,21 +59,20 @@ export default function PageContentEditor() {
// posts all the tags at once as an array obj // posts all the tags at once as an array obj
async function handleSave() { async function handleSave() {
setSubmitStatus('validating'); setSubmitStatus(createInputStatus(STATUS_PROCESSING));
await postConfigUpdateToAPI({ await postConfigUpdateToAPI({
apiPath: API_CUSTOM_CONTENT, apiPath: API_CUSTOM_CONTENT,
data: { value: content }, data: { value: content },
onSuccess: () => { onSuccess: (message: string) => {
setFieldInConfigState({ setFieldInConfigState({
fieldName: 'extraPageContent', fieldName: 'extraPageContent',
value: content, value: content,
path: 'instanceDetails', path: 'instanceDetails',
}); });
setSubmitStatus('success'); setSubmitStatus(createInputStatus(STATUS_SUCCESS, message));
}, },
onError: (message: string) => { onError: (message: string) => {
setSubmitStatus('error'); setSubmitStatus(createInputStatus(STATUS_ERROR, message));
setSubmitStatusMessage(`There was an error: ${message}`);
}, },
}); });
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
@@ -79,9 +82,6 @@ export default function PageContentEditor() {
setContent(initialContent); setContent(initialContent);
}, [instanceDetails]); }, [instanceDetails]);
const { icon: newStatusIcon = null, message: newStatusMessage = '' } =
SUCCESS_STATES[submitStatus] || {};
return ( return (
<div className="config-page-content-form"> <div className="config-page-content-form">
<Title level={2}>Edit custom content</Title> <Title level={2}>Edit custom content</Title>
@@ -107,9 +107,8 @@ export default function PageContentEditor() {
Save Save
</Button> </Button>
) : null} ) : null}
<div className={`status-message ${submitStatus || ''}`}> <FormStatusIndicator status={submitStatus} />
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
</div>
</div> </div>
</div> </div>
); );

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { Typography } from 'antd'; import { Typography } from 'antd';
import Link from 'next/link'; import Link from 'next/link';
import configStyles from '../styles/config-pages.module.scss';
import EditInstanceDetails from './components/config/edit-instance-details'; import EditInstanceDetails from './components/config/edit-instance-details';
const { Title } = Typography; const { Title } = Typography;
@@ -12,14 +11,12 @@ export default function PublicFacingDetails() {
<> <>
<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="edit-public-details-container">
<div className={configStyles.textFieldsSection}> <EditInstanceDetails />
<EditInstanceDetails />
<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>
</Link> </Link>
</div>
</div> </div>
</> </>
); );

View File

@@ -1,111 +1,14 @@
import React, { useContext, useState, useEffect } from 'react'; import { Typography } from 'antd';
import { ServerStatusContext } from '../utils/server-status-context'; import React from 'react';
import { Typography, Switch, Input, Button } from 'antd'; import EditStorage from './components/config/edit-storage';
import {
postConfigUpdateToAPI,
API_S3_INFO,
} from './components/config/constants';
const { Title } = Typography; const { Title } = Typography;
function Storage({ config }) { export default function ConfigStorageInfo() {
if (!config || !config.s3) {
return null;
}
const [endpoint, setEndpoint] = useState(config.s3.endpoint);
const [accessKey, setAccessKey] = useState(config.s3.accessKey);
const [secret, setSecret] = useState(config.s3.secret);
const [bucket, setBucket] = useState(config.s3.bucket);
const [region, setRegion] = useState(config.s3.region);
const [acl, setAcl] = useState(config.s3.acl);
const [servingEndpoint, setServingEndpoint] = useState(config.s3.servingEndpoint);
const [enabled, setEnabled] = useState(config.s3.enabled);
function storageEnabledChanged(storageEnabled) {
setEnabled(storageEnabled);
}
function endpointChanged(e) {
setEndpoint(e.target.value)
}
function accessKeyChanged(e) {
setAccessKey(e.target.value)
}
function secretChanged(e) {
setSecret(e.target.value)
}
function bucketChanged(e) {
setBucket(e.target.value)
}
function regionChanged(e) {
setRegion(e.target.value)
}
function aclChanged(e) {
setAcl(e.target.value)
}
function servingEndpointChanged(e) {
setServingEndpoint(e.target.value)
}
async function save() {
const payload = {
value: {
enabled: enabled,
endpoint: endpoint,
accessKey: accessKey,
secret: secret,
bucket: bucket,
region: region,
acl: acl,
servingEndpoint: servingEndpoint,
}
};
try {
await postConfigUpdateToAPI({apiPath: API_S3_INFO, data: payload});
} catch(e) {
console.error(e);
}
}
const table = enabled ? (
<>
<br></br>
endpoint <Input defaultValue={endpoint} value={endpoint} onChange={endpointChanged} />
access key<Input label="Access key" defaultValue={accessKey} value={accessKey} onChange={accessKeyChanged} />
secret <Input label="Secret" defaultValue={secret} value={secret} onChange={secretChanged} />
bucket <Input label="Bucket" defaultValue={bucket} value={bucket} onChange={bucketChanged} />
region <Input label="Region" defaultValue={region} value={region} onChange={regionChanged} />
advanced<br></br>
acl <Input label="ACL" defaultValue={acl} value={acl} onChange={aclChanged} />
serving endpoint <Input label="Serving endpoint" defaultValue={servingEndpoint} value={servingEndpoint} onChange={servingEndpointChanged} />
<Button onClick={save}>Save</Button>
</>
): null;
return ( return (
<> <>
<Title level={2}>Storage</Title> <Title level={2}>Storage</Title>
Enabled: <EditStorage />
<Switch checked={enabled} onChange={storageEnabledChanged} />
{ table }
</> </>
); );
} }
export default function ServerConfig() {
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig: config } = serverStatusData || {};
return (
<div>
<Storage config={config} />
</div>
);
}

View File

@@ -1,35 +1,79 @@
// Base styles for form-textfield, form-textfield-with-submit, and helper components.
/* STATUS-CONTAINER BASE */
.status-container {
&.status-success {
color: var(--ant-success);
}
&.status-error {
color: var(--ant-error);
}
&.status-warning {
color: var(--ant-warning);
}
&.empty {
display: none;
}
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
font-size: .75rem;
.status-icon {
display: inline-block;
margin-right: .5em;
}
}
/* TIP CONTAINER BASE */
.field-tip {
font-size: .7em;
color: rgba(255,255,255,.5)
}
/* TEXTFIELD-CONTAINER BASE */
.textfield-container { .textfield-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: flex-start; align-items: flex-start;
justify-content: flex-start; justify-content: flex-start;
width: 100%; width: 100%;
max-width: 600px;
.label-side { .label-side {
padding-right: 1em; padding-right: .75em;
text-align: right; text-align: right;
width: 12rem; width: 12em;
margin: .2em 0; margin: .2em 0;
} }
.textfield-label { .textfield-label {
font-weight: 400; font-weight: 400;
font-size: .85rem; font-size: .85em;
color: var(--owncast-purple); color: var(--owncast-purple);
}
.required-label {
color: var(--ant-error);
}
&::after {
content: ':';
}
}
&.required {
.textfield-label {
&::before {
content: '*';
display: inline-block;
margin-right: .25em;
color: var(--ant-error);
}
}
}
.input-side { .input-side {
max-width: 500px; max-width: 500px;
width: 100%; width: 100%;
} }
.input-group {
.input-group,
.status-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
@@ -37,49 +81,118 @@
} }
.status-container { .status-container {
margin: 0 .25em; margin: .25em;
min-height: 1.5em; width: 100%;
font-size: .75em; display: block;
&.empty {
.status-icon { display: none;
display: inline-block; visibility: visible;
margin-right: .5em;
} }
} }
.tip { .field-tip {
margin: .5em .5em; margin: .5em .5em;
font-size: .75rem;
color: rgba(255,255,255,.75);
} }
@media (max-width: 800px) { @media (max-width: 800px) {
// flex-direction: column;
flex-wrap: wrap; flex-wrap: wrap;
.label-side { .label-side {
width: 100%;
text-align: left; text-align: left;
} }
} }
} }
.status-message { /* TEXTFIELD-WITH-SUBMIT-CONTAINER BASE */
// margin: 1rem 0;
// min-height: 1.4em;
// font-size: .75rem;
&.success {
color: var(--ant-success);
}
&.error {
color: var(--ant-error);
}
}
.textfield-with-submit-container { .textfield-with-submit-container {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
.update-button-container { align-items: flex-start;
display: inline-block; margin-bottom: 1em;
margin: .25em;
.textfield-component {
width: 100%;
.textfield-container {
.field-tip,
.status-container {
display: none;
}
}
}
// for lack of a better name
.lower-container {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
.label-spacer {
width: 12em;
}
.lower-content {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-end;
width: 100%;
.field-tip {
margin-right: 1em;
width: 100%;
}
.status-container {
margin: .5em;
&.empty {
display: none;
}
}
}
.update-button-container {
visibility: hidden;
margin: .25em 0;
}
}
&.submittable {
.lower-container {
.update-button-container {
visibility: visible;
}
}
}
@media (max-width: 800px) {
.label-spacer {
display: none;
}
}
}
/* TOGGLE SWITCH-WITH-SUBMIT-CONTAINER BASE */
.toggleswitch-container {
.status-container {
margin-top: .25rem;
}
.toggleswitch {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
.label {
font-weight: bold;
color: var(--owncast-purple);
}
.info-tip {
margin-left: .5rem;
svg {
fill: white;
}
}
.ant-form-item {
margin: 0 .75rem 0 0;
}
} }
} }

View File

@@ -1,33 +0,0 @@
.publicDetailsContainer {
display: flex;
flex-direction: row;
align-items: flex-start;
flex-wrap: wrap;
.textFieldsSection {
margin-right: 2rem;
}
}
.socialLinksEditor {
width: 20rem;
margin: 2em 0;
}
.tag-editor-container,
.config-directory-details-form {
border-radius: 1em;
background-color: rgba(128,99,255,.1);
padding: 1.5em;
margin-bottom: 1em;
}
//////////////////////////////
// common?
.dataTable {
}

View File

@@ -98,30 +98,30 @@
// form-toggleswitch // form-toggleswitch
// form-toggleswitch // form-toggleswitch
.toggleswitch-container { // .toggleswitch-container {
.status-message { // .status-message {
margin-top: .25rem; // margin-top: .25rem;
} // }
} // }
.toggleswitch { // .toggleswitch {
display: flex; // display: flex;
flex-direction: row; // flex-direction: row;
align-items: center; // align-items: center;
justify-content: flex-start; // justify-content: flex-start;
.label { // .label {
font-weight: bold; // font-weight: bold;
color: var(--owncast-purple); // color: var(--owncast-purple);
} // }
.info-tip { // .info-tip {
margin-left: .5rem; // margin-left: .5rem;
svg { // svg {
fill: white; // fill: white;
} // }
} // }
.ant-form-item { // .ant-form-item {
margin: 0 .75rem 0 0; // margin: 0 .75rem 0 0;
} // }
} // }
// TAGS STUFF // TAGS STUFF
// TAGS STUFF // TAGS STUFF
@@ -321,3 +321,62 @@
// } // }
// } // }
// } // }
// EDIT STORAGE
.edit-storage-container {
.form-fields {
display: none;
margin-bottom: 1em;
}
&.enabled {
.form-fields {
display: block;
}
}
.button-container {
margin: 1em 0;
}
.advanced-section {
margin: 1em 0;
}
}
.field-container {
padding: .85em 0 .5em;
&:nth-child(even) {
background-color: rgba(0,0,0,.25);
}
}
.field-streamkey-container {
.left-side {
display: flex;
flex-direction: row;
align-items: flex-start;
}
.textfield-with-submit-container {
margin-bottom: 0;
.textfield-container {
max-width: 400px;
}
}
.streamkey-actions {
max-width: 90px;
button {
margin: .25em;
}
}
.streamkey-notice {
font-size: 0.75em;
color: var(--ant-error);
margin-left: 12em;
margin-bottom: 1em;
}
}

View File

@@ -63,11 +63,22 @@ export interface VideoSettingsFields {
cpuUsageLevel: CpuUsageLevel; cpuUsageLevel: CpuUsageLevel;
} }
export interface S3Field {
acl?: string;
accessKey: string;
bucket: string;
enabled: boolean;
endpoint: string;
region: string;
secret: string;
servingEndpoint?: string;
}
export interface ConfigDetails { export interface ConfigDetails {
ffmpegPath: string; ffmpegPath: string;
instanceDetails: ConfigInstanceDetailsFields; instanceDetails: ConfigInstanceDetailsFields;
rtmpServerPort: string; rtmpServerPort: string;
s3: any; // tbd s3: S3Field;
streamKey: string; streamKey: string;
webServerPort: string; webServerPort: string;
yp: ConfigDirectoryFields; yp: ConfigDirectoryFields;

View File

@@ -13,12 +13,7 @@ export const STATUS_PROCESSING = 'proessing';
export const STATUS_SUCCESS = 'success'; export const STATUS_SUCCESS = 'success';
export const STATUS_WARNING = 'warning'; export const STATUS_WARNING = 'warning';
export type InputStatusTypes = export type InputStatusTypes = 'error' | 'invalid' | 'proessing' | 'success' | 'warning';
| typeof STATUS_ERROR
| typeof STATUS_INVALID
| typeof STATUS_PROCESSING
| typeof STATUS_SUCCESS
| typeof STATUS_WARNING;
export type StatusState = { export type StatusState = {
type: InputStatusTypes; type: InputStatusTypes;
@@ -28,22 +23,27 @@ export type StatusState = {
export const INPUT_STATES = { export const INPUT_STATES = {
[STATUS_SUCCESS]: { [STATUS_SUCCESS]: {
type: STATUS_SUCCESS,
icon: <CheckCircleFilled style={{ color: 'green' }} />, icon: <CheckCircleFilled style={{ color: 'green' }} />,
message: 'Success!', message: 'Success!',
}, },
[STATUS_ERROR]: { [STATUS_ERROR]: {
type: STATUS_ERROR,
icon: <ExclamationCircleFilled style={{ color: 'red' }} />, icon: <ExclamationCircleFilled style={{ color: 'red' }} />,
message: 'An error occurred.', message: 'An error occurred.',
}, },
[STATUS_INVALID]: { [STATUS_INVALID]: {
type: STATUS_INVALID,
icon: <ExclamationCircleFilled style={{ color: 'red' }} />, icon: <ExclamationCircleFilled style={{ color: 'red' }} />,
message: 'An error occurred.', message: 'An error occurred.',
}, },
[STATUS_PROCESSING]: { [STATUS_PROCESSING]: {
type: STATUS_PROCESSING,
icon: <LoadingOutlined />, icon: <LoadingOutlined />,
message: '', message: '',
}, },
[STATUS_WARNING]: { [STATUS_WARNING]: {
type: STATUS_WARNING,
icon: <WarningOutlined style={{ color: '#fc0' }} />, icon: <WarningOutlined style={{ color: '#fc0' }} />,
message: '', message: '',
}, },

View File

@@ -23,7 +23,16 @@ export const initialServerConfigState: ConfigDetails = {
ffmpegPath: '', ffmpegPath: '',
rtmpServerPort: '', rtmpServerPort: '',
webServerPort: '', webServerPort: '',
s3: {}, s3: {
accessKey: '',
acl: '',
bucket: '',
enabled: false,
endpoint: '',
region: '',
secret: '',
servingEndpoint: '',
},
yp: { yp: {
enabled: false, enabled: false,
instanceUrl: '', instanceUrl: '',
@@ -32,7 +41,7 @@ export const initialServerConfigState: ConfigDetails = {
latencyLevel: 4, latencyLevel: 4,
cpuUsageLevel: 3, cpuUsageLevel: 3,
videoQualityVariants: [DEFAULT_VARIANT_STATE], videoQualityVariants: [DEFAULT_VARIANT_STATE],
} },
}; };
const initialServerStatusState = { const initialServerStatusState = {
@@ -51,7 +60,9 @@ export const ServerStatusContext = React.createContext({
...initialServerStatusState, ...initialServerStatusState,
serverConfig: initialServerConfigState, serverConfig: initialServerConfigState,
setFieldInConfigState: (args: UpdateArgs) => { return args }, setFieldInConfigState: (args: UpdateArgs) => {
return args;
},
}); });
const ServerStatusProvider = ({ children }) => { const ServerStatusProvider = ({ children }) => {
@@ -62,7 +73,6 @@ const ServerStatusProvider = ({ children }) => {
try { try {
const result = await fetchData(STATUS); const result = await fetchData(STATUS);
setStatus({ ...result }); setStatus({ ...result });
} catch (error) { } catch (error) {
// todo // todo
} }
@@ -77,22 +87,21 @@ const ServerStatusProvider = ({ children }) => {
}; };
const setFieldInConfigState = ({ fieldName, value, path }: UpdateArgs) => { const setFieldInConfigState = ({ fieldName, value, path }: UpdateArgs) => {
const updatedConfig = path ? const updatedConfig = path
{ ? {
...config, ...config,
[path]: { [path]: {
...config[path], ...config[path],
[fieldName]: value, [fieldName]: value,
}, },
} : }
{ : {
...config, ...config,
[fieldName]: value, [fieldName]: value,
}; };
setConfig(updatedConfig); setConfig(updatedConfig);
}; };
useEffect(() => { useEffect(() => {
let getStatusIntervalId = null; let getStatusIntervalId = null;
@@ -104,21 +113,19 @@ const ServerStatusProvider = ({ children }) => {
// returned function will be called on component unmount // returned function will be called on component unmount
return () => { return () => {
clearInterval(getStatusIntervalId); clearInterval(getStatusIntervalId);
} };
}, []); }, []);
const providerValue = { const providerValue = {
...status, ...status,
serverConfig: config, serverConfig: config,
setFieldInConfigState, setFieldInConfigState,
}; };
return ( return (
<ServerStatusContext.Provider value={providerValue}> <ServerStatusContext.Provider value={providerValue}>{children}</ServerStatusContext.Provider>
{children}
</ServerStatusContext.Provider>
); );
} };
ServerStatusProvider.propTypes = { ServerStatusProvider.propTypes = {
children: PropTypes.element.isRequired, children: PropTypes.element.isRequired,