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 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 (
<div className={`publicDetailsContainer`}>
<div className={`textFieldsSection`}>
<TextFieldWithSubmit
fieldName="streamKey"
{...TEXTFIELD_PROPS_STREAM_KEY}
value={formDataValues.streamKey}
initialValue={streamKey}
type={TEXTFIELD_TYPE_PASSWORD}
onChange={handleFieldChange}
/>
<div>
<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
dashboard!
</span>
<Tooltip className="copy-tooltip" title="Copied!" trigger="" visible={copyIsVisible}>
<Button type="primary" icon={<CopyOutlined />} size="small" onClick={copyStreamKey} />
</Tooltip>
<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 className="edit-public-details-container">
<TextFieldWithSubmit
fieldName="streamKey"
{...TEXTFIELD_PROPS_STREAM_KEY}
value={formDataValues.streamKey}
initialValue={streamKey}
type={TEXTFIELD_TYPE_PASSWORD}
onChange={handleFieldChange}
/>
<div>
<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
dashboard!
</span>
<Tooltip className="copy-tooltip" title="Copied!" trigger="" visible={copyIsVisible}>
<Button type="primary" icon={<CopyOutlined />} size="small" onClick={copyStreamKey} />
</Tooltip>
<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>
);
}

View File

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

View File

@ -11,14 +11,12 @@ export default function PublicFacingDetails() {
<>
<Title level={2}>Edit your public facing instance details</Title>
<div className={`publicDetailsContainer`}>
<div className={`textFieldsSection`}>
<EditInstanceDetails />
<div className="edit-public-details-container">
<EditInstanceDetails />
<Link href="/admin/config-page-content">
<a>Edit your extra page content here.</a>
</Link>
</div>
<Link href="/admin/config-page-content">
<a>Edit your extra page content here.</a>
</Link>
</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 { UpdateArgs } from '../types/config-section';
import { ServerStatusContext } from '../utils/server-status-context';
import { Typography, Switch, Input, Button } from 'antd';
import {
postConfigUpdateToAPI,
API_S3_INFO,
RESET_TIMEOUT,
S3_TEXT_FIELDS_INFO,
} 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 }) {
if (!config || !config.s3) {
const { Title } = Typography;
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;
}
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);
let resetTimer = null;
const resetStates = () => {
setSubmitStatus(null);
resetTimer = null;
clearTimeout(resetTimer);
};
function storageEnabledChanged(storageEnabled) {
setEnabled(storageEnabled);
}
// update individual values in state
const handleFieldChange = ({ fieldName, value }: UpdateArgs) => {
setFormDataValues({
...formDataValues,
[fieldName]: value,
});
};
function endpointChanged(e) {
setEndpoint(e.target.value)
}
// posts the whole state
const handleSave = async () => {
setSubmitStatus(createInputStatus(STATUS_PROCESSING));
const postValue = formDataValues;
function accessKeyChanged(e) {
setAccessKey(e.target.value)
}
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);
},
});
};
function secretChanged(e) {
setSecret(e.target.value)
}
// toggle switch.
const handleSwitchChange = (storageEnabled: boolean) => {
setShouldDisplayForm(storageEnabled);
handleFieldChange({ fieldName: 'enabled', value: storageEnabled });
function bucketChanged(e) {
setBucket(e.target.value)
}
// if current data in current store says s3 is enabled,
// we should save this state
// if (!storageEnabled && s3.enabled) {
// handleSave();
// }
};
function regionChanged(e) {
setRegion(e.target.value)
}
const containerClass = classNames({
'edit-storage-container': true,
enabled: shouldDisplayForm,
});
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;
const isSaveable = checkSaveable(formDataValues, s3);
return (
<>
<Title level={2}>Storage</Title>
Enabled:
<Switch checked={enabled} onChange={storageEnabledChanged} />
{ table }
</>
);
}
<div className={containerClass}>
<div className="enable-switch">
<Switch
checked={formDataValues.enabled}
defaultChecked={formDataValues.enabled}
onChange={handleSwitchChange}
checkedChildren="ON"
unCheckedChildren="OFF"
/>{' '}
Enabled
</div>
export default function ServerConfig() {
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig: config } = serverStatusData || {};
<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>
return (
<div>
<Storage config={config} />
<Collapse>
<Panel header="Advanced Settings" key="1">
<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>
);
}
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
.toggleswitch-container {
.status-message {
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;
}
}
// .toggleswitch-container {
// .status-message {
// 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;
// }
// }
// 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;
}
export interface S3Field {
acl?: string;
accessKey: string;
bucket: string;
enabled: boolean;
endpoint: string;
region: string;
secret: string;
servingEndpoint?: string;
}
export interface ConfigDetails {
ffmpegPath: string;
instanceDetails: ConfigInstanceDetailsFields;
rtmpServerPort: string;
s3: any; // tbd
s3: S3Field;
streamKey: string;
webServerPort: string;
yp: ConfigDirectoryFields;

View File

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