Reorganize admin components to help bundling

This commit is contained in:
Gabe Kangas
2023-01-09 20:57:29 -08:00
parent 29882f1291
commit 7392ae8a54
67 changed files with 138 additions and 126 deletions

View File

@@ -0,0 +1,259 @@
import { 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 { AlertMessageContext } from '../../../../utils/alert-message-context';
import {
postConfigUpdateToAPI,
API_S3_INFO,
RESET_TIMEOUT,
S3_TEXT_FIELDS_INFO,
} from '../../../../utils/config-constants';
import {
createInputStatus,
StatusState,
STATUS_ERROR,
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../../../../utils/input-statuses';
import { TextField } from '../../TextField';
import { FormStatusIndicator } from '../../FormStatusIndicator';
import { isValidUrl } from '../../../../utils/urls';
import { ToggleSwitch } from '../../ToggleSwitch';
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,
forcePathStyle,
} = formValues;
// if fields are filled out and different from what's in store, then return true
if (enabled) {
if (!!endpoint && isValidUrl(endpoint) && !!accessKey && !!secret && !!bucket && !!region) {
if (
enabled !== currentValues.enabled ||
endpoint !== currentValues.endpoint ||
accessKey !== currentValues.accessKey ||
secret !== currentValues.secret ||
bucket !== currentValues.bucket ||
region !== currentValues.region ||
(!currentValues.servingEndpoint && servingEndpoint !== '') ||
(!!currentValues.servingEndpoint && servingEndpoint !== currentValues.servingEndpoint) ||
(!currentValues.acl && acl !== '') ||
(!!currentValues.acl && acl !== currentValues.acl) ||
forcePathStyle !== currentValues.forcePathStyle
) {
return true;
}
}
} else if (enabled !== currentValues.enabled) {
return true;
}
return false;
}
// eslint-disable-next-line react/function-component-definition
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 { setMessage: setAlertMessage } = useContext(AlertMessageContext);
const { s3 } = serverConfig;
const {
accessKey = '',
acl = '',
bucket = '',
enabled = false,
endpoint = '',
region = '',
secret = '',
servingEndpoint = '',
forcePathStyle = false,
} = s3;
useEffect(() => {
setFormDataValues({
accessKey,
acl,
bucket,
enabled,
endpoint,
region,
secret,
servingEndpoint,
forcePathStyle,
});
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);
setAlertMessage(
'Changing your storage configuration will take place the next time you start a new stream.',
);
},
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 });
};
const handleForcePathStyleSwitchChange = (forcePathStyleEnabled: boolean) => {
handleFieldChange({ fieldName: 'forcePathStyle', value: forcePathStyleEnabled });
};
const containerClass = classNames({
'edit-storage-container': true,
'form-module': true,
enabled: shouldDisplayForm,
});
const isSaveable = checkSaveable(formDataValues, s3);
return (
<div className={containerClass}>
<div className="enable-switch">
<ToggleSwitch
apiPath=""
fieldName="enabled"
label="Use S3 Storage Provider"
checked={formDataValues.enabled}
onChange={handleSwitchChange}
/>
{/* <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="Optional 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>
<div className="enable-switch">
<ToggleSwitch
{...S3_TEXT_FIELDS_INFO.forcePathStyle}
fieldName="forcePathStyle"
checked={formDataValues.forcePathStyle}
onChange={handleForcePathStyleSwitchChange}
/>
</div>
</Panel>
</Collapse>
</div>
<div className="button-container">
<Button type="primary" onClick={handleSave} disabled={!isSaveable}>
Save
</Button>
<FormStatusIndicator status={submitStatus} />
</div>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import EditInstanceDetails from '../../EditInstanceDetails2';
// eslint-disable-next-line react/function-component-definition
export default function ConfigServerDetails() {
return (
<div className="config-server-details-form">
<p className="description">
You should change your admin password from the default and keep it safe. For most people
it&apos;s likely the other settings will not need to be changed.
</p>
<div className="form-module config-server-details-container">
<EditInstanceDetails />
</div>
</div>
);
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
import EditStorage from './EditStorage';
// eslint-disable-next-line react/function-component-definition
export default function ConfigStorageInfo() {
return (
<>
<p className="description">
Owncast supports optionally using external storage providers to stream your video. Learn
more about this by visiting our{' '}
<a
href="https://owncast.online/docs/storage/?source=admin"
target="_blank"
rel="noopener noreferrer"
>
Storage Documentation
</a>
.
</p>
<p className="description">
Configuring this incorrectly will likely cause your video to be unplayable. Double check the
documentation for your storage provider on how to configure the bucket you created for
Owncast.
</p>
<p className="description">
Keep in mind this is for live streaming, not for archival, recording or VOD purposes.
</p>
<EditStorage />
</>
);
}

View File

@@ -0,0 +1,172 @@
import React, { useContext, useState } from 'react';
import { Table, Space, Button, Typography, Alert, Input, Form } from 'antd';
import { DeleteOutlined, EyeOutlined, PlusOutlined } from '@ant-design/icons';
import { ServerStatusContext } from '../../../../utils/server-status-context';
import { fetchData, UPDATE_STREAM_KEYS } from '../../../../utils/apis';
const { Paragraph } = Typography;
const { Item } = Form;
const saveKeys = async (keys, setError) => {
try {
await fetchData(UPDATE_STREAM_KEYS, {
method: 'POST',
auth: true,
data: { value: keys },
});
} catch (error) {
console.error(error);
setError(error);
}
};
const AddKeyForm = ({ setShowAddKeyForm, setFieldInConfigState, streamKeys, setError }) => {
const handleAddKey = (newkey: any) => {
const updatedKeys = [...streamKeys, newkey];
setFieldInConfigState({
fieldName: 'streamKeys',
value: updatedKeys,
});
saveKeys(updatedKeys, setError);
setShowAddKeyForm(false);
};
return (
<Form layout="inline" autoComplete="off" onFinish={handleAddKey}>
<Item label="Key" name="key" tooltip="The key you provide your broadcasting software">
<Input placeholder="def456" />
</Item>
<Item label="Comment" name="comment" tooltip="For remembering why you added this key">
<Input placeholder="My OBS Key" />
</Item>
<Button type="primary" htmlType="submit">
Add
</Button>
</Form>
);
};
const AddKeyButton = ({ setShowAddKeyForm }) => (
<Button type="default" onClick={() => setShowAddKeyForm(true)}>
<PlusOutlined />
</Button>
);
const StreamKeys = () => {
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { streamKeys } = serverConfig;
const [showAddKeyForm, setShowAddKeyForm] = useState(false);
const [showKeyMap, setShowKeyMap] = useState({});
const [error, setError] = useState(null);
const handleDeleteKey = keyToRemove => {
const newKeys = streamKeys.filter(k => k !== keyToRemove);
setFieldInConfigState({
fieldName: 'streamKeys',
value: newKeys,
});
saveKeys(newKeys, setError);
};
const handleToggleShowKey = key => {
setShowKeyMap({
...showKeyMap,
[key]: !showKeyMap[key],
});
};
const columns = [
{
title: 'Key',
dataIndex: 'key',
key: 'key',
render: text => (
<Space direction="horizontal">
<Paragraph copyable>{showKeyMap[text] ? text : '**********'}</Paragraph>
<Button
type="link"
style={{ top: '-7px' }}
icon={<EyeOutlined />}
onClick={() => handleToggleShowKey(text)}
/>
</Space>
),
},
{
title: 'Comment',
dataIndex: 'comment',
key: 'comment',
},
{
title: '',
key: 'delete',
render: text => <Button onClick={() => handleDeleteKey(text)} icon={<DeleteOutlined />} />,
},
];
return (
<div>
<Paragraph>
A streaming key is used with your broadcasting software to authenticate itself to Owncast.
Most people will only need one. However, if you share a server with others or you want
different keys for different broadcasting sources you can add more here.
</Paragraph>
<Paragraph>
These keys are unrelated to the admin password and will not grant you access to make changes
to Owncast&apos;s configuration.
</Paragraph>
<Paragraph>
Read more about broadcasting at{' '}
<a
href="https://owncast.online/docs/broadcasting/?source=admin"
target="_blank"
rel="noopener noreferrer"
>
the documentation
</a>
.
</Paragraph>
<Space direction="vertical" style={{ width: '70%' }}>
{error && <Alert type="error" message="Saving Keys Error" description={error} />}
{streamKeys.length === 0 && (
<Alert
message="No stream keys!"
description="You will not be able to stream until you create at least one stream key and add it to your broadcasting software."
type="error"
/>
)}
<Table
rowKey="key"
columns={columns}
dataSource={streamKeys}
pagination={false}
// eslint-disable-next-line react/no-unstable-nested-components
footer={() =>
showAddKeyForm ? (
<AddKeyForm
setShowAddKeyForm={setShowAddKeyForm}
streamKeys={streamKeys}
setFieldInConfigState={setFieldInConfigState}
setError={setError}
/>
) : (
<AddKeyButton setShowAddKeyForm={setShowAddKeyForm} />
)
}
/>
<br />
</Space>
</div>
);
};
export default StreamKeys;