0

revise Storage forms, and add basic validation to it; misc field cleanup

This commit is contained in:
gingervitis 2021-01-31 23:40:39 -08:00
parent 05167f77e5
commit 7501cfc548
9 changed files with 445 additions and 199 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

@ -63,50 +63,48 @@ export default function EditInstanceDetails() {
} }
return ( return (
<div className={`publicDetailsContainer`}> <div className="edit-public-details-container">
<div className={`textFieldsSection`}> <TextFieldWithSubmit
<TextFieldWithSubmit fieldName="streamKey"
fieldName="streamKey" {...TEXTFIELD_PROPS_STREAM_KEY}
{...TEXTFIELD_PROPS_STREAM_KEY} value={formDataValues.streamKey}
value={formDataValues.streamKey} initialValue={streamKey}
initialValue={streamKey} type={TEXTFIELD_TYPE_PASSWORD}
type={TEXTFIELD_TYPE_PASSWORD} onChange={handleFieldChange}
onChange={handleFieldChange} />
/> <div>
<div> <span style={{ fontSize: '0.75em', color: '#ff7777', marginRight: '0.5em' }}>
<span style={{ fontSize: '0.75em', color: '#ff7777', marginRight: '0.5em' }}> Save this key somewhere safe, you will need it to stream or login to the admin
Save this key somewhere safe, you will need it to stream or login to the admin dashboard!
dashboard! </span>
</span> <Tooltip className="copy-tooltip" title="Copied!" trigger="" visible={copyIsVisible}>
<Tooltip className="copy-tooltip" title="Copied!" trigger="" visible={copyIsVisible}> <Button type="primary" icon={<CopyOutlined />} size="small" onClick={copyStreamKey} />
<Button type="primary" icon={<CopyOutlined />} size="small" onClick={copyStreamKey} /> </Tooltip>
</Tooltip> <Button type="primary" icon={<RedoOutlined />} size="small" onClick={generateStreamKey} />
<Button type="primary" icon={<RedoOutlined />} size="small" onClick={generateStreamKey} />
</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

@ -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

@ -11,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={`publicDetailsContainer`}> <div className="edit-public-details-container">
<div className={`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,230 @@
import { Typography, Switch, Button, Collapse } from 'antd';
import classNames from 'classnames';
import React, { useContext, useState, useEffect } from 'react'; import React, { useContext, useState, useEffect } from 'react';
import { UpdateArgs } from '../types/config-section';
import { ServerStatusContext } from '../utils/server-status-context'; import { ServerStatusContext } from '../utils/server-status-context';
import { Typography, Switch, Input, Button } from 'antd';
import { import {
postConfigUpdateToAPI, postConfigUpdateToAPI,
API_S3_INFO, API_S3_INFO,
RESET_TIMEOUT,
S3_TEXT_FIELDS_INFO,
} from './components/config/constants'; } from './components/config/constants';
const { Title } = Typography; import {
createInputStatus,
StatusState,
STATUS_ERROR,
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../utils/input-statuses';
import TextField from './components/config/form-textfield';
import InputStatusInfo from './components/config/input-status-info';
function Storage({ config }) { const { Title } = Typography;
if (!config || !config.s3) { 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.bucket ||
region !== currentValues.region ||
servingEndpoint !== currentValues.servingEndpoint ||
acl !== currentValues.acl
) {
return true;
}
}
} else if (enabled !== currentValues.enabled) {
return true;
}
return false;
}
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; return null;
} }
const [endpoint, setEndpoint] = useState(config.s3.endpoint); let resetTimer = null;
const [accessKey, setAccessKey] = useState(config.s3.accessKey); const resetStates = () => {
const [secret, setSecret] = useState(config.s3.secret); setSubmitStatus(null);
const [bucket, setBucket] = useState(config.s3.bucket); resetTimer = null;
const [region, setRegion] = useState(config.s3.region); clearTimeout(resetTimer);
const [acl, setAcl] = useState(config.s3.acl); };
const [servingEndpoint, setServingEndpoint] = useState(config.s3.servingEndpoint);
const [enabled, setEnabled] = useState(config.s3.enabled);
function storageEnabledChanged(storageEnabled) { // update individual values in state
setEnabled(storageEnabled); const handleFieldChange = ({ fieldName, value }: UpdateArgs) => {
} setFormDataValues({
...formDataValues,
[fieldName]: value,
});
};
function endpointChanged(e) { // posts the whole state
setEndpoint(e.target.value) const handleSave = async () => {
} setSubmitStatus(createInputStatus(STATUS_PROCESSING));
const postValue = formDataValues;
function accessKeyChanged(e) { await postConfigUpdateToAPI({
setAccessKey(e.target.value) 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);
},
});
};
function secretChanged(e) { // toggle switch.
setSecret(e.target.value) const handleSwitchChange = (storageEnabled: boolean) => {
} setShouldDisplayForm(storageEnabled);
handleFieldChange({ fieldName: 'enabled', value: storageEnabled });
function bucketChanged(e) { // if current data in current store says s3 is enabled,
setBucket(e.target.value) // we should save this state
} // if (!storageEnabled && s3.enabled) {
// handleSave();
// }
};
function regionChanged(e) { const containerClass = classNames({
setRegion(e.target.value) 'edit-storage-container': true,
} enabled: shouldDisplayForm,
});
function aclChanged(e) { const isSaveable = checkSaveable(formDataValues, s3);
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 (
<> <div className={containerClass}>
<Title level={2}>Storage</Title> <div className="enable-switch">
Enabled: <Switch
<Switch checked={enabled} onChange={storageEnabledChanged} /> checked={formDataValues.enabled}
{ table } defaultChecked={formDataValues.enabled}
</> onChange={handleSwitchChange}
); checkedChildren="ON"
} unCheckedChildren="OFF"
/>{' '}
Enabled
</div>
export default function ServerConfig() { <div className="form-fields">
const serverStatusData = useContext(ServerStatusContext); <div className="field-container">
const { serverConfig: config } = serverStatusData || {}; <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>
return ( <Collapse>
<div> <Panel header="Advanced Settings" key="1">
<Storage config={config} /> <Title level={4}>Advanced</Title>
<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 onClick={handleSave} disabled={!isSaveable}>
Save
</Button>
<InputStatusInfo status={submitStatus} />
</div>
</div> </div>
); );
} }
export default function ConfigStorageInfo() {
return (
<>
<Title level={2}>Storage</Title>
<EditStorage />
</>
);
}

View File

@ -169,3 +169,30 @@
} }
} }
} }
/* 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

@ -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,23 @@
// } // }
// } // }
// } // }
// EDIT STORAGE
.edit-storage-container {
.form-fields {
display: none;
margin-bottom: 1em;
}
&.enabled {
.form-fields {
display: block;
}
}
.button-container {
margin: 1em 0;
}
}

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

@ -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,