diff --git a/web/pages/components/config/constants.tsx b/web/pages/components/config/constants.tsx index 1de2f3fe2..cf6ec3606 100644 --- a/web/pages/components/config/constants.tsx +++ b/web/pages/components/config/constants.tsx @@ -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: '', + }, +}; diff --git a/web/pages/components/config/edit-server-details.tsx b/web/pages/components/config/edit-server-details.tsx index 20c40f663..2b048c7e5 100644 --- a/web/pages/components/config/edit-server-details.tsx +++ b/web/pages/components/config/edit-server-details.tsx @@ -63,50 +63,48 @@ export default function EditInstanceDetails() { } return ( -
-
- -
- - Save this key somewhere safe, you will need it to stream or login to the admin - dashboard! - - -
- - - +
+ +
+ + Save this key somewhere safe, you will need it to stream or login to the admin + dashboard! + + +
+ + +
); } diff --git a/web/pages/components/config/edit-tags.tsx b/web/pages/components/config/edit-tags.tsx index 9be437ab6..f04c2a2c0 100644 --- a/web/pages/components/config/edit-tags.tsx +++ b/web/pages/components/config/edit-tags.tsx @@ -19,7 +19,7 @@ const { Title } = Typography; export default function EditInstanceTags() { const [newTagInput, setNewTagInput] = useState(''); - const [fieldStatus, setFieldStatus] = useState(null); + const [submitStatus, setSubmitStatus] = useState(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} />
diff --git a/web/pages/config-public-details.tsx b/web/pages/config-public-details.tsx index b18adefd0..3b3009d7a 100644 --- a/web/pages/config-public-details.tsx +++ b/web/pages/config-public-details.tsx @@ -11,14 +11,12 @@ export default function PublicFacingDetails() { <> Edit your public facing instance details -
-
- +
+ - - Edit your extra page content here. - -
+ + Edit your extra page content here. +
); diff --git a/web/pages/config-storage.tsx b/web/pages/config-storage.tsx index e46713350..9266e035b 100644 --- a/web/pages/config-storage.tsx +++ b/web/pages/config-storage.tsx @@ -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; - -function Storage({ config }) { - 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) - } +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 servingEndpointChanged(e) { - setServingEndpoint(e.target.value) +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; +} - async function save() { - const payload = { - value: { - enabled: enabled, - endpoint: endpoint, - accessKey: accessKey, - secret: secret, - bucket: bucket, - region: region, - acl: acl, - servingEndpoint: servingEndpoint, - } - }; +function EditStorage() { + const [formDataValues, setFormDataValues] = useState(null); + const [submitStatus, setSubmitStatus] = useState(null); - try { - await postConfigUpdateToAPI({apiPath: API_S3_INFO, data: payload}); - } catch(e) { - console.error(e); - } + 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 table = enabled ? ( - <> -

- endpoint - access key - secret - bucket - region - advanced

- acl - serving endpoint - - - ): 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 ( - <> - Storage - Enabled: - - { table } - +
+
+ {' '} + Enabled +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + + + Advanced + +
+ +
+
+ +
+
+
+
+ +
+ + +
+
); } -export default function ServerConfig() { - const serverStatusData = useContext(ServerStatusContext); - const { serverConfig: config } = serverStatusData || {}; - +export default function ConfigStorageInfo() { return ( -
- -
+ <> + Storage + + ); } diff --git a/web/styles/config-formfields.scss b/web/styles/config-formfields.scss index e49ce5c89..53b8fd03a 100644 --- a/web/styles/config-formfields.scss +++ b/web/styles/config-formfields.scss @@ -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; + } + } +} diff --git a/web/styles/config.scss b/web/styles/config.scss index 9af5cf087..66f568234 100644 --- a/web/styles/config.scss +++ b/web/styles/config.scss @@ -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; + } +} diff --git a/web/types/config-section.ts b/web/types/config-section.ts index 25854a0a1..e85036397 100644 --- a/web/types/config-section.ts +++ b/web/types/config-section.ts @@ -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; diff --git a/web/utils/server-status-context.tsx b/web/utils/server-status-context.tsx index 16f82255c..3c2b2517c 100644 --- a/web/utils/server-status-context.tsx +++ b/web/utils/server-status-context.tsx @@ -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 ( - - {children} - + {children} ); -} +}; ServerStatusProvider.propTypes = { children: PropTypes.element.isRequired, }; -export default ServerStatusProvider; \ No newline at end of file +export default ServerStatusProvider;