Browse Source

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

pull/1886/head
gingervitis 6 years ago
parent
commit
7501cfc548
  1. 66
      web/pages/components/config/constants.tsx
  2. 84
      web/pages/components/config/edit-server-details.tsx
  3. 20
      web/pages/components/config/edit-tags.tsx
  4. 12
      web/pages/config-public-details.tsx
  5. 301
      web/pages/config-storage.tsx
  6. 27
      web/styles/config-formfields.scss
  7. 68
      web/styles/config.scss
  8. 13
      web/types/config-section.ts
  9. 61
      web/utils/server-status-context.tsx

66
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 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: '',
},
};

84
web/pages/components/config/edit-server-details.tsx

@ -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>
); );
} }

20
web/pages/components/config/edit-tags.tsx

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

12
web/pages/config-public-details.tsx

@ -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>
</> </>
); );

301
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 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,
function Storage({ config }) { StatusState,
if (!config || !config.s3) { STATUS_ERROR,
return null; STATUS_PROCESSING,
} STATUS_SUCCESS,
} from '../utils/input-statuses';
const [endpoint, setEndpoint] = useState(config.s3.endpoint); import TextField from './components/config/form-textfield';
const [accessKey, setAccessKey] = useState(config.s3.accessKey); import InputStatusInfo from './components/config/input-status-info';
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) { const { Title } = Typography;
setServingEndpoint(e.target.value) 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() { function EditStorage() {
const payload = { const [formDataValues, setFormDataValues] = useState(null);
value: { const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
enabled: enabled,
endpoint: endpoint,
accessKey: accessKey,
secret: secret,
bucket: bucket,
region: region,
acl: acl,
servingEndpoint: servingEndpoint,
}
};
try { const [shouldDisplayForm, setShouldDisplayForm] = useState(false);
await postConfigUpdateToAPI({apiPath: API_S3_INFO, data: payload}); const serverStatusData = useContext(ServerStatusContext);
} catch(e) { const { serverConfig, setFieldInConfigState } = serverStatusData || {};
console.error(e);
} 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 ? ( let resetTimer = null;
<> const resetStates = () => {
<br></br> setSubmitStatus(null);
endpoint <Input defaultValue={endpoint} value={endpoint} onChange={endpointChanged} /> resetTimer = null;
access key<Input label="Access key" defaultValue={accessKey} value={accessKey} onChange={accessKeyChanged} /> clearTimeout(resetTimer);
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} /> // update individual values in state
advanced<br></br> const handleFieldChange = ({ fieldName, value }: UpdateArgs) => {
acl <Input label="ACL" defaultValue={acl} value={acl} onChange={aclChanged} /> setFormDataValues({
serving endpoint <Input label="Serving endpoint" defaultValue={servingEndpoint} value={servingEndpoint} onChange={servingEndpointChanged} /> ...formDataValues,
<Button onClick={save}>Save</Button> [fieldName]: value,
</> });
): null; };
// 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 ( 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>
<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>
<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 ServerConfig() { export default function ConfigStorageInfo() {
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig: config } = serverStatusData || {};
return ( return (
<div> <>
<Storage config={config} /> <Title level={2}>Storage</Title>
</div> <EditStorage />
</>
); );
} }

27
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;
}
}
}

68
web/styles/config.scss

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

13
web/types/config-section.ts

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

61
web/utils/server-status-context.tsx

@ -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;
@ -101,27 +110,25 @@ const ServerStatusProvider = ({ children }) => {
getConfig(); getConfig();
// 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,
}; };
export default ServerStatusProvider; export default ServerStatusProvider;

Loading…
Cancel
Save