Admin css overhaul pt2 (#19)

* tweaks to offline state in admin viewers page

If stream is offline, hide current viewers statistic and viewers table.
Also, change wording for describing max viewers.

* take out ant dark stylesheet, organize ant color overrides

* remove ant dark css; cleanup ant overrides; format public-detail page

* combine toggleswitch component style with textfield so layout can be shared

* fix toggleswitch status message placement

* - update styles for modals, collapses
- move reset dir into its own component
- assorted style cleanups ans consistencies

* hide entire advanced section for resetyp if no yp

* temp adjustments to video modal

* temp comment out toggle switch use for later'

* address PR comments

* lint

* update type

* allow warnings during lint

Co-authored-by: nebunez <uoj2y7wak869@opayq.net>
This commit is contained in:
gingervitis
2021-02-12 23:55:59 -08:00
committed by GitHub
parent 250acbf6e9
commit a122ee6c42
40 changed files with 1150 additions and 552 deletions

View File

@@ -19,8 +19,11 @@ const TOOLTIPS = {
4: 'high',
5: 'highest',
};
export default function CPUUsageSelector({ defaultValue, onChange }) {
interface Props {
defaultValue: number;
onChange: (arg: number) => void;
}
export default function CPUUsageSelector({ defaultValue, onChange }: Props) {
const [selectedOption, setSelectedOption] = useState(null);
const serverStatusData = useContext(ServerStatusContext);
@@ -42,10 +45,14 @@ export default function CPUUsageSelector({ defaultValue, onChange }) {
return (
<div className="config-video-segements-conatiner">
<Title level={3}>CPU Usage</Title>
<p>There are trade-offs when considering CPU usage blah blah more wording here.</p>
<br />
<Title level={3} className="section-title">
CPU Usage
</Title>
<p className="description">
There are trade-offs when considering CPU usage blah blah more wording here.
</p>
<br />
<div className="segment-slider-container">
<Slider
tipFormatter={value => TOOLTIPS[value]}

View File

@@ -33,9 +33,11 @@ export default function EditYPDetails() {
}
return (
<div className="config-directory-details-form">
<Title level={3}>Owncast Directory Settings</Title>
<Title level={3} className="section-title">
Owncast Directory Settings
</Title>
<p>
<p className="description">
Would you like to appear in the{' '}
<a href="https://directory.owncast.online" target="_blank" rel="noreferrer">
<strong>Owncast Directory</strong>

View File

@@ -1,4 +1,6 @@
import React, { useState, useContext, useEffect } from 'react';
import { Typography } from 'antd';
import TextFieldWithSubmit, {
TEXTFIELD_TYPE_TEXTAREA,
TEXTFIELD_TYPE_URL,
@@ -12,9 +14,14 @@ import {
TEXTFIELD_PROPS_SERVER_SUMMARY,
TEXTFIELD_PROPS_LOGO,
API_YP_SWITCH,
FIELD_PROPS_YP,
FIELD_PROPS_NSFW,
} from '../../utils/config-constants';
import { UpdateArgs } from '../../types/config-section';
import ToggleSwitch from './form-toggleswitch-with-submit';
const { Title } = Typography;
export default function EditInstanceDetails() {
const [formDataValues, setFormDataValues] = useState(null);
@@ -22,6 +29,7 @@ export default function EditInstanceDetails() {
const { serverConfig } = serverStatusData || {};
const { instanceDetails, yp } = serverConfig;
const { instanceUrl } = yp;
useEffect(() => {
setFormDataValues({
@@ -53,40 +61,74 @@ export default function EditInstanceDetails() {
});
};
return (
<div className={`publicDetailsContainer`}>
<div className={`textFieldsSection`}>
<TextFieldWithSubmit
fieldName="instanceUrl"
{...TEXTFIELD_PROPS_INSTANCE_URL}
value={formDataValues.instanceUrl}
initialValue={yp.instanceUrl}
type={TEXTFIELD_TYPE_URL}
onChange={handleFieldChange}
onSubmit={handleSubmitInstanceUrl}
/>
const hasInstanceUrl = instanceUrl !== '';
<TextFieldWithSubmit
fieldName="name"
{...TEXTFIELD_PROPS_SERVER_NAME}
value={formDataValues.name}
initialValue={instanceDetails.name}
onChange={handleFieldChange}
return (
<div className="edit-general-settings">
<Title level={3} className="section-title">
Configure Instance Details
</Title>
<br />
<TextFieldWithSubmit
fieldName="instanceUrl"
{...TEXTFIELD_PROPS_INSTANCE_URL}
value={formDataValues.instanceUrl}
initialValue={yp.instanceUrl}
type={TEXTFIELD_TYPE_URL}
onChange={handleFieldChange}
onSubmit={handleSubmitInstanceUrl}
/>
<TextFieldWithSubmit
fieldName="name"
{...TEXTFIELD_PROPS_SERVER_NAME}
value={formDataValues.name}
initialValue={instanceDetails.name}
onChange={handleFieldChange}
/>
<TextFieldWithSubmit
fieldName="summary"
{...TEXTFIELD_PROPS_SERVER_SUMMARY}
type={TEXTFIELD_TYPE_TEXTAREA}
value={formDataValues.summary}
initialValue={instanceDetails.summary}
onChange={handleFieldChange}
/>
<TextFieldWithSubmit
fieldName="logo"
{...TEXTFIELD_PROPS_LOGO}
value={formDataValues.logo}
initialValue={instanceDetails.logo}
onChange={handleFieldChange}
/>
<br />
<Title level={3} className="section-title">
Owncast Directory Settings
</Title>
<p className="description">
Would you like to appear in the{' '}
<a href="https://directory.owncast.online" target="_blank" rel="noreferrer">
<strong>Owncast Directory</strong>
</a>
?
</p>
<div className="config-yp-container">
<ToggleSwitch
fieldName="enabled"
useSubmit
{...FIELD_PROPS_YP}
checked={formDataValues.enabled}
disabled={!hasInstanceUrl}
/>
<TextFieldWithSubmit
fieldName="summary"
{...TEXTFIELD_PROPS_SERVER_SUMMARY}
type={TEXTFIELD_TYPE_TEXTAREA}
value={formDataValues.summary}
initialValue={instanceDetails.summary}
onChange={handleFieldChange}
/>
<TextFieldWithSubmit
fieldName="logo"
{...TEXTFIELD_PROPS_LOGO}
value={formDataValues.logo}
initialValue={instanceDetails.logo}
onChange={handleFieldChange}
<ToggleSwitch
fieldName="nsfw"
useSubmit
{...FIELD_PROPS_NSFW}
checked={formDataValues.nsfw}
disabled={!hasInstanceUrl}
/>
</div>
</div>

View File

@@ -0,0 +1,118 @@
// EDIT CUSTOM DETAILS ON YOUR PAGE
import React, { useState, useEffect, useContext } from 'react';
import { Typography, Button } from 'antd';
import dynamic from 'next/dynamic';
import MarkdownIt from 'markdown-it';
import { ServerStatusContext } from '../../utils/server-status-context';
import {
postConfigUpdateToAPI,
RESET_TIMEOUT,
API_CUSTOM_CONTENT,
} from '../../utils/config-constants';
import {
createInputStatus,
StatusState,
STATUS_ERROR,
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator';
import 'react-markdown-editor-lite/lib/index.css';
const mdParser = new MarkdownIt(/* Markdown-it options */);
const MdEditor = dynamic(() => import('react-markdown-editor-lite'), {
ssr: false,
});
const { Title } = Typography;
export default function EditPageContent() {
const [content, setContent] = useState('');
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const [hasChanged, setHasChanged] = useState(false);
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { instanceDetails } = serverConfig;
const { extraPageContent: initialContent } = instanceDetails;
let resetTimer = null;
function handleEditorChange({ text }) {
setContent(text);
if (text !== initialContent && !hasChanged) {
setHasChanged(true);
} else if (text === initialContent && hasChanged) {
setHasChanged(false);
}
}
// Clear out any validation states and messaging
const resetStates = () => {
setSubmitStatus(null);
setHasChanged(false);
clearTimeout(resetTimer);
resetTimer = null;
};
// posts all the tags at once as an array obj
async function handleSave() {
setSubmitStatus(createInputStatus(STATUS_PROCESSING));
await postConfigUpdateToAPI({
apiPath: API_CUSTOM_CONTENT,
data: { value: content },
onSuccess: (message: string) => {
setFieldInConfigState({
fieldName: 'extraPageContent',
value: content,
path: 'instanceDetails',
});
setSubmitStatus(createInputStatus(STATUS_SUCCESS, message));
},
onError: (message: string) => {
setSubmitStatus(createInputStatus(STATUS_ERROR, message));
},
});
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}
useEffect(() => {
setContent(initialContent);
}, [instanceDetails]);
return (
<div className="edit-page-content">
<Title level={3} className="section-title">
Custom Page Content
</Title>
<p className="description">
Edit the content of your page by using simple{' '}
<a href="https://www.markdownguide.org/basic-syntax/">Markdown syntax</a>.
</p>
<MdEditor
style={{ height: '30em' }}
value={content}
renderHTML={(c: string) => mdParser.render(c)}
onChange={handleEditorChange}
config={{
htmlClass: 'markdown-editor-preview-pane',
markdownClass: 'markdown-editor-pane',
}}
/>
<div className="page-content-actions">
{hasChanged ? (
<Button type="primary" onClick={handleSave}>
Save
</Button>
) : null}
<FormStatusIndicator status={submitStatus} />
</div>
</div>
);
}

View File

@@ -1,7 +1,6 @@
import React, { useState, useContext, useEffect } from 'react';
import { Button, Tooltip, Collapse, Popconfirm } from 'antd';
import { Button, Tooltip, Collapse } from 'antd';
import { CopyOutlined, RedoOutlined } from '@ant-design/icons';
const { Panel } = Collapse;
import { TEXTFIELD_TYPE_NUMBER, TEXTFIELD_TYPE_PASSWORD } from './form-textfield';
import TextFieldWithSubmit from './form-textfield-with-submit';
@@ -15,9 +14,11 @@ import {
TEXTFIELD_PROPS_STREAM_KEY,
TEXTFIELD_PROPS_WEB_PORT,
} from '../../utils/config-constants';
import { fetchData, API_YP_RESET } from '../../utils/apis';
import { UpdateArgs } from '../../types/config-section';
import ResetYP from './reset-yp';
const { Panel } = Collapse;
export default function EditInstanceDetails() {
const [formDataValues, setFormDataValues] = useState(null);
@@ -68,41 +69,6 @@ export default function EditInstanceDetails() {
}
};
const resetDirectoryRegistration = async () => {
try {
await fetchData(API_YP_RESET);
setMessage('');
} catch (error) {
alert(error);
}
};
function ResetYP() {
// TODO: Uncomment this after it's styled.
// if (yp.enabled) {
return (
<div className="field-container">
Reset Directory:
<Popconfirm
placement="topLeft"
title={'Are you sure you want to reset your connection to the Owncast directory?'}
onConfirm={resetDirectoryRegistration}
okText="Yes"
cancelText="No"
>
<Button>Reset Directory Connection</Button>
</Popconfirm>
<p>
If you are experiencing issues with your listing on the Owncast Directory and were asked
to "reset" your connection to the service, you can do that here. The next time you go live
it will try and re-register your server with the directory from scratch.
</p>
</div>
);
// }
// return null;
}
function generateStreamKey() {
let key = '';
for (let i = 0; i < 3; i += 1) {
@@ -172,13 +138,14 @@ export default function EditInstanceDetails() {
onChange={handleFieldChange}
onSubmit={showConfigurationRestartMessage}
/>
<Collapse>
<Panel header="Advanced Settings" key="1">
<div className="form-fields">
{yp.enabled && (
<Collapse className="advanced-settings">
<Panel header="Advanced Settings" key="1">
<ResetYP />
</div>
</Panel>
</Collapse>
</Panel>
</Collapse>
)}
</div>
);
}

View File

@@ -165,43 +165,36 @@ export default function EditSocialLinks() {
const socialHandlesColumns: ColumnsType<SocialHandle> = [
{
title: '#',
dataIndex: 'key',
key: 'key',
},
{
title: 'Platform',
dataIndex: 'platform',
key: 'platform',
render: (platform: string) => {
title: 'Social Link',
dataIndex: '',
key: 'combo',
render: (data, record) => {
const { platform, url } = record;
const platformInfo = availableIconsList.find(item => item.key === platform);
if (!platformInfo) {
return platform;
}
const { icon, platform: platformName } = platformInfo;
return (
<>
<div className="social-handle-cell">
<span className="option-icon">
<img src={`${NEXT_PUBLIC_API_HOST}${icon}`} alt="" className="option-icon" />
</span>
<span className="option-label">{platformName}</span>
</>
<p className="option-label">
<strong>{platformName}</strong>
<span>{url}</span>
</p>
</div>
);
},
},
{
title: 'Url Link',
dataIndex: 'url',
key: 'url',
},
{
title: '',
dataIndex: '',
key: 'edit',
render: (data, record, index) => {
return (
<span className="actions">
<div className="actions">
<Button
type="primary"
size="small"
@@ -219,7 +212,7 @@ export default function EditSocialLinks() {
size="small"
onClick={() => handleDeleteItem(index)}
/>
</span>
</div>
);
},
},
@@ -231,12 +224,17 @@ export default function EditSocialLinks() {
return (
<div className="social-links-edit-container">
<p>Add all your social media handles and links to your other profiles here.</p>
<Title level={3} className="section-title">
Your Social Handles
</Title>
<p className="description">
Add all your social media handles and links to your other profiles here.
</p>
<FormStatusIndicator status={submitStatus} />
<Table
className="dataTable"
className="social-handles-table"
pagination={false}
size="small"
rowKey={record => record.url}

View File

@@ -21,6 +21,7 @@ import {
import TextField from './form-textfield';
import FormStatusIndicator from './form-status-indicator';
import { isValidUrl } from '../../utils/urls';
// import ToggleSwitch from './form-toggleswitch-with-submit';
const { Panel } = Collapse;
@@ -135,6 +136,7 @@ export default function EditStorage() {
const containerClass = classNames({
'edit-storage-container': true,
'form-module': true,
enabled: shouldDisplayForm,
});
@@ -143,6 +145,12 @@ export default function EditStorage() {
return (
<div className={containerClass}>
<div className="enable-switch">
{/* <ToggleSwitch
fieldName="enabled"
label="Storage Enabled"
checked={formDataValues.enabled}
onChange={handleSwitchChange}
/> */}
<Switch
checked={formDataValues.enabled}
defaultChecked={formDataValues.enabled}

View File

@@ -21,6 +21,8 @@ import {
const { Title } = Typography;
const TAG_COLOR = '#5a67d8';
export default function EditInstanceTags() {
const [newTagInput, setNewTagInput] = useState<string>('');
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
@@ -100,8 +102,12 @@ export default function EditInstanceTags() {
return (
<div className="tag-editor-container">
<Title level={3}>Add Tags</Title>
<p>This is a great way to categorize your Owncast server on the Directory!</p>
<Title level={3} className="section-title">
Add Tags
</Title>
<p className="description">
This is a great way to categorize your Owncast server on the Directory!
</p>
<div className="tag-current-tags">
{tags.map((tag, index) => {
@@ -109,7 +115,7 @@ export default function EditInstanceTags() {
handleDeleteTag(index);
};
return (
<Tag closable onClose={handleClose} key={`tag-${tag}-${index}`}>
<Tag closable onClose={handleClose} color={TAG_COLOR} key={`tag-${tag}-${index}`}>
{tag}
</Tag>
);

View File

@@ -120,7 +120,7 @@ export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
onChange={handleChange}
/>
</div>
<div className="textfield-container lower-container">
<div className="formfield-container lower-container">
<p className="label-spacer" />
<div className="lower-content">
<div className="field-tip">{tip}</div>

View File

@@ -108,6 +108,7 @@ export default function TextField(props: TextFieldProps) {
const { type: statusType } = status || {};
const containerClass = classNames({
'formfield-container': true,
'textfield-container': true,
[`type-${type}`]: true,
required,
@@ -117,7 +118,7 @@ export default function TextField(props: TextFieldProps) {
<div className={containerClass}>
{label ? (
<div className="label-side">
<label htmlFor={fieldId} className="textfield-label">
<label htmlFor={fieldId} className="formfield-label">
{label}
</label>
</div>
@@ -140,10 +141,7 @@ export default function TextField(props: TextFieldProps) {
/>
</div>
<FormStatusIndicator status={status} />
<p className="field-tip">
{tip}
{/* <InfoTip tip={tip} /> */}
</p>
<p className="field-tip">{tip}</p>
</div>
</div>
);
@@ -151,9 +149,7 @@ export default function TextField(props: TextFieldProps) {
TextField.defaultProps = {
className: '',
// configPath: '',
disabled: false,
// initialValue: '',
label: '',
maxLength: 255,

View File

@@ -1,3 +1,6 @@
// This is a wrapper for the Ant Switch component.
// onChange of the switch, it will automatically post a change to the config api.
import React, { useState, useContext } from 'react';
import { Switch } from 'antd';
import {
@@ -12,7 +15,6 @@ import FormStatusIndicator from './form-status-indicator';
import { RESET_TIMEOUT, postConfigUpdateToAPI } from '../../utils/config-constants';
import { ServerStatusContext } from '../../utils/server-status-context';
import InfoTip from '../info-tip';
interface ToggleSwitchProps {
apiPath: string;
@@ -23,8 +25,9 @@ interface ToggleSwitchProps {
disabled?: boolean;
label?: string;
tip?: string;
useSubmit?: boolean;
onChange?: (arg: boolean) => void;
}
export default function ToggleSwitch(props: ToggleSwitchProps) {
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
@@ -33,7 +36,17 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
const serverStatusData = useContext(ServerStatusContext);
const { setFieldInConfigState } = serverStatusData || {};
const { apiPath, checked, configPath = '', disabled = false, fieldName, label, tip } = props;
const {
apiPath,
checked,
configPath = '',
disabled = false,
fieldName,
label,
tip,
useSubmit,
onChange,
} = props;
const resetStates = () => {
setSubmitStatus(null);
@@ -42,41 +55,52 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
};
const handleChange = async (isChecked: boolean) => {
setSubmitStatus(createInputStatus(STATUS_PROCESSING));
if (useSubmit) {
setSubmitStatus(createInputStatus(STATUS_PROCESSING));
await postConfigUpdateToAPI({
apiPath,
data: { value: isChecked },
onSuccess: () => {
setFieldInConfigState({ fieldName, value: isChecked, path: configPath });
setSubmitStatus(createInputStatus(STATUS_SUCCESS));
},
onError: (message: string) => {
setSubmitStatus(createInputStatus(STATUS_ERROR, `There was an error: ${message}`));
},
});
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
await postConfigUpdateToAPI({
apiPath,
data: { value: isChecked },
onSuccess: () => {
setFieldInConfigState({ fieldName, value: isChecked, path: configPath });
setSubmitStatus(createInputStatus(STATUS_SUCCESS));
},
onError: (message: string) => {
setSubmitStatus(createInputStatus(STATUS_ERROR, `There was an error: ${message}`));
},
});
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}
if (onChange) {
onChange(isChecked);
}
};
const loading = submitStatus !== null && submitStatus.type === STATUS_PROCESSING;
return (
<div className="toggleswitch-container">
<div className="toggleswitch">
<Switch
className={`switch field-${fieldName}`}
loading={loading}
onChange={handleChange}
defaultChecked={checked}
checked={checked}
checkedChildren="ON"
unCheckedChildren="OFF"
disabled={disabled}
/>
<span className="label">
{label} <InfoTip tip={tip} />
</span>
<div className="formfield-container toggleswitch-container">
{label && (
<div className="label-side">
<span className="formfield-label">{label}</span>
</div>
)}
<div className="input-side">
<div className="input-group">
<Switch
className={`switch field-${fieldName}`}
loading={loading}
onChange={handleChange}
defaultChecked={checked}
checked={checked}
checkedChildren="ON"
unCheckedChildren="OFF"
disabled={disabled}
/>
<FormStatusIndicator status={submitStatus} />
</div>
<p className="field-tip">{tip}</p>
</div>
<FormStatusIndicator status={submitStatus} />
</div>
);
}
@@ -87,4 +111,6 @@ ToggleSwitch.defaultProps = {
disabled: false,
label: '',
tip: '',
useSubmit: false,
onChange: null,
};

View File

@@ -0,0 +1,42 @@
import { Popconfirm, Button, Typography } from 'antd';
import { useContext } from 'react';
import { AlertMessageContext } from '../../utils/alert-message-context';
import { API_YP_RESET, fetchData } from '../../utils/apis';
export default function ResetYP() {
const { setMessage } = useContext(AlertMessageContext);
const { Title } = Typography;
const resetDirectoryRegistration = async () => {
try {
await fetchData(API_YP_RESET);
setMessage('');
} catch (error) {
alert(error);
}
};
return (
<>
<Title level={3} className="section-title">
Reset Directory
</Title>
<p className="description">
If you are experiencing issues with your listing on the Owncast Directory and were asked to
&quot;reset&quot; your connection to the service, you can do that here. The next time you go
live it will try and re-register your server with the directory from scratch.
</p>
<Popconfirm
placement="topLeft"
title="Are you sure you want to reset your connection to the Owncast directory?"
onConfirm={resetDirectoryRegistration}
okText="Yes"
cancelText="No"
>
<Button type="primary">Reset Directory Connection</Button>
</Popconfirm>
</>
);
}

View File

@@ -19,11 +19,11 @@ export default function SocialDropdown({ iconList, selectedOption, onSelected }:
const inititalSelected = selectedOption === '' ? null : selectedOption;
return (
<div className="social-dropdown-container">
<p className="">
<p className="description">
If you are looking for a platform name not on this list, please select Other and type in
your own name. A logo will not be provided.
</p>
<p className="">
<p className="description">
If you DO have a logo, drop it in to the <code>/webroot/img/platformicons</code> directory
and update the <code>/socialHandle.go</code> list. Then restart the server and it will show
up in the list.

View File

@@ -46,9 +46,6 @@ function SegmentToolTip({ value }: SegmentToolTipProps) {
export default function VideoLatency() {
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
// const [submitStatus, setSubmitStatus] = useState(null);
// const [submitStatusMessage, setSubmitStatusMessage] = useState('');
const [selectedOption, setSelectedOption] = useState(null);
const serverStatusData = useContext(ServerStatusContext);
@@ -68,7 +65,6 @@ export default function VideoLatency() {
const resetStates = () => {
setSubmitStatus(null);
// setSubmitStatusMessage('');
resetTimer = null;
clearTimeout(resetTimer);
};
@@ -88,8 +84,6 @@ export default function VideoLatency() {
});
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Latency buffer level updated.'));
// setSubmitStatus('success');
// setSubmitStatusMessage('Variants updated.');
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
if (serverStatusData.online) {
setMessage(
@@ -100,8 +94,6 @@ export default function VideoLatency() {
onError: (message: string) => {
setSubmitStatus(createInputStatus(STATUS_ERROR, message));
// setSubmitStatus('error');
// setSubmitStatusMessage(message);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
},
});
@@ -113,15 +105,19 @@ export default function VideoLatency() {
return (
<div className="config-video-segements-conatiner">
<Title level={3}>Latency Buffer</Title>
<p>
While it's natural to want to keep your latency as low as possible, you may experience
<Title level={3} className="section-title">
Latency Buffer
</Title>
<p className="description">
While it&apos;s natural to want to keep your latency as low as possible, you may experience
reduced error tolerance and stability in some environments the lower you go.
</p>
For interactive live streams you may want to experiment with a lower latency, for
non-interactive broadcasts you may want to increase it.{' '}
<a href="https://owncast.online/docs/encoding#latency-buffer">Read to learn more.</a>
<p></p>
<p className="description">
For interactive live streams you may want to experiment with a lower latency, for
non-interactive broadcasts you may want to increase it.{' '}
<a href="https://owncast.online/docs/encoding#latency-buffer">Read to learn more.</a>
</p>
<div className="segment-slider-container">
<Slider
tipFormatter={value => <SegmentToolTip value={SLIDER_COMMENTS[value]} />}
@@ -132,8 +128,8 @@ export default function VideoLatency() {
defaultValue={selectedOption}
value={selectedOption}
/>
<FormStatusIndicator status={submitStatus} />
</div>
<FormStatusIndicator status={submitStatus} />
</div>
);
}

View File

@@ -1,11 +1,12 @@
// This content populates the video variant modal, which is spawned from the variants table.
import React from 'react';
import { Slider, Switch, Collapse } from 'antd';
import { Slider, Switch, Collapse, Typography } from 'antd';
import { FieldUpdaterFunc, VideoVariant, UpdateArgs } from '../../types/config-section';
import TextField from './form-textfield';
import { DEFAULT_VARIANT_STATE } from '../../utils/config-constants';
import InfoTip from '../info-tip';
import CPUUsageSelector from './cpu-usage';
// import ToggleSwitch from './form-toggleswitch-with-submit';
const { Panel } = Collapse;
@@ -55,7 +56,6 @@ const VIDEO_VARIANT_DEFAULTS = {
tip: "Optionally resize this content's height.",
},
};
interface VideoVariantFormProps {
dataState: VideoVariant;
onUpdateField: FieldUpdaterFunc;
@@ -79,6 +79,7 @@ export default function VideoVariantForm({
};
const handleScaledWidthChanged = (args: UpdateArgs) => {
const value = Number(args.value);
// eslint-disable-next-line no-restricted-globals
if (isNaN(value)) {
return;
}
@@ -86,6 +87,7 @@ export default function VideoVariantForm({
};
const handleScaledHeightChanged = (args: UpdateArgs) => {
const value = Number(args.value);
// eslint-disable-next-line no-restricted-globals
if (isNaN(value)) {
return;
}
@@ -108,124 +110,123 @@ export default function VideoVariantForm({
return (
<div className="config-variant-form">
<div className="section-intro">
<p className="description">
Say a thing here about how this all works. Read more{' '}
<a href="https://owncast.online/docs/configuration/">here</a>.
<br />
<br />
</div>
</p>
{/* ENCODER PRESET FIELD */}
<div className="field">
<div className="form-component">
<CPUUsageSelector
defaultValue={dataState.cpuUsageLevel}
onChange={handleVideoCpuUsageLevelChange}
/>
{selectedPresetNote ? (
<span className="selected-value-note">{selectedPresetNote}</span>
) : null}
</div>
</div>
{/* VIDEO PASSTHROUGH FIELD */}
<div style={{ display: 'none' }}>
<div className="field">
<p className="label">
<InfoTip tip={VIDEO_VARIANT_DEFAULTS.videoPassthrough.tip} />
Use Video Passthrough?
</p>
<div className="form-component">
<Switch
defaultChecked={dataState.videoPassthrough}
checked={dataState.videoPassthrough}
onChange={handleVideoPassChange}
checkedChildren="Yes"
unCheckedChildren="No"
/>
</div>
</div>
</div>
{/* VIDEO BITRATE FIELD */}
<div className={`field ${dataState.videoPassthrough ? 'disabled' : ''}`}>
<p className="label">
<InfoTip tip={VIDEO_VARIANT_DEFAULTS.videoBitrate.tip} />
Video Bitrate:
</p>
<div className="form-component">
<Slider
tipFormatter={value => `${value} ${videoBRUnit}`}
disabled={dataState.videoPassthrough === true}
defaultValue={dataState.videoBitrate}
value={dataState.videoBitrate}
onChange={handleVideoBitrateChange}
step={videoBitrateDefaults.incrementBy}
min={videoBRMin}
max={videoBRMax}
marks={{
[videoBRMin]: `${videoBRMin} ${videoBRUnit}`,
[videoBRMax]: `${videoBRMax} ${videoBRUnit}`,
}}
/>
{selectedVideoBRnote ? (
<span className="selected-value-note">{selectedVideoBRnote}</span>
) : null}
</div>
</div>
<Collapse>
<Panel header="Advanced Settings" key="1">
<div className="section-intro">
Resizing your content will take additional resources on your server. If you wish to
optionally resize your output for this stream variant then you should either set the
width <strong>or</strong> the height to keep your aspect ratio.
</div>
<div className="field">
<TextField
type="number"
{...VIDEO_VARIANT_DEFAULTS.scaledWidth}
value={dataState.scaledWidth}
onChange={handleScaledWidthChanged}
/>
</div>
<div className="field">
<TextField
type="number"
{...VIDEO_VARIANT_DEFAULTS.scaledHeight}
value={dataState.scaledHeight}
onChange={handleScaledHeightChanged}
<div className="row">
<div>
{/* ENCODER PRESET FIELD */}
<div className="form-module cpu-usage-container">
<CPUUsageSelector
defaultValue={dataState.cpuUsageLevel}
onChange={handleVideoCpuUsageLevelChange}
/>
{selectedPresetNote && (
<span className="selected-value-note">{selectedPresetNote}</span>
)}
</div>
{/* FRAME RATE FIELD */}
<div className="field">
{/* VIDEO PASSTHROUGH FIELD */}
<div style={{ display: 'none' }} className="form-module">
<p className="label">
<InfoTip tip={VIDEO_VARIANT_DEFAULTS.framerate.tip} />
Frame rate:
<InfoTip tip={VIDEO_VARIANT_DEFAULTS.videoPassthrough.tip} />
Use Video Passthrough?
</p>
<div className="form-component">
<Slider
// tooltipVisible
tipFormatter={value => `${value} ${framerateUnit}`}
defaultValue={dataState.framerate}
value={dataState.framerate}
onChange={handleFramerateChange}
step={framerateDefaults.incrementBy}
min={framerateMin}
max={framerateMax}
marks={{
[framerateMin]: `${framerateMin} ${framerateUnit}`,
[framerateMax]: `${framerateMax} ${framerateUnit}`,
}}
{/* todo: change to ToggleSwitch for layout */}
<Switch
defaultChecked={dataState.videoPassthrough}
checked={dataState.videoPassthrough}
onChange={handleVideoPassChange}
// label="Use Video Passthrough"
checkedChildren="Yes"
unCheckedChildren="No"
/>
{selectedFramerateNote ? (
<span className="selected-value-note">{selectedFramerateNote}</span>
) : null}
</div>
</div>
</Panel>
</Collapse>
{/* VIDEO BITRATE FIELD */}
<div className={`form-module ${dataState.videoPassthrough ? 'disabled' : ''}`}>
<Typography.Title level={3} className="section-title">
Video Bitrate
</Typography.Title>
<p className="description">{VIDEO_VARIANT_DEFAULTS.videoBitrate.tip}</p>
<div className="segment-slider-container">
<Slider
tipFormatter={value => `${value} ${videoBRUnit}`}
disabled={dataState.videoPassthrough === true}
defaultValue={dataState.videoBitrate}
value={dataState.videoBitrate}
onChange={handleVideoBitrateChange}
step={videoBitrateDefaults.incrementBy}
min={videoBRMin}
max={videoBRMax}
marks={{
[videoBRMin]: `${videoBRMin} ${videoBRUnit}`,
[videoBRMax]: `${videoBRMax} ${videoBRUnit}`,
}}
/>
{selectedVideoBRnote && (
<span className="selected-value-note">{selectedVideoBRnote}</span>
)}
</div>
</div>
</div>
<Collapse className="advanced-settings">
<Panel header="Advanced Settings" key="1">
<div className="section-intro">
Resizing your content will take additional resources on your server. If you wish to
optionally resize your output for this stream variant then you should either set the
width <strong>or</strong> the height to keep your aspect ratio.
</div>
<div className="field">
<TextField
type="number"
{...VIDEO_VARIANT_DEFAULTS.scaledWidth}
value={dataState.scaledWidth}
onChange={handleScaledWidthChanged}
/>
</div>
<div className="field">
<TextField
type="number"
{...VIDEO_VARIANT_DEFAULTS.scaledHeight}
value={dataState.scaledHeight}
onChange={handleScaledHeightChanged}
/>
</div>
{/* FRAME RATE FIELD */}
<div className="field">
<p className="label">
<InfoTip tip={VIDEO_VARIANT_DEFAULTS.framerate.tip} />
Frame rate:
</p>
<div className="segment-slider-container form-component">
<Slider
// tooltipVisible
tipFormatter={value => `${value} ${framerateUnit}`}
defaultValue={dataState.framerate}
value={dataState.framerate}
onChange={handleFramerateChange}
step={framerateDefaults.incrementBy}
min={framerateMin}
max={framerateMax}
marks={{
[framerateMin]: `${framerateMin} ${framerateUnit}`,
[framerateMax]: `${framerateMax} ${framerateUnit}`,
}}
/>
{selectedFramerateNote ? (
<span className="selected-value-note">{selectedFramerateNote}</span>
) : null}
</div>
</div>
</Panel>
</Collapse>
</div>
</div>
);
}

View File

@@ -12,10 +12,17 @@ import VideoVariantForm from './video-variant-form';
import {
API_VIDEO_VARIANTS,
DEFAULT_VARIANT_STATE,
SUCCESS_STATES,
RESET_TIMEOUT,
postConfigUpdateToAPI,
} from '../../utils/config-constants';
import {
createInputStatus,
StatusState,
STATUS_ERROR,
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator';
const { Title } = Typography;
@@ -36,8 +43,7 @@ export default function CurrentVariantsTable() {
// current data inside modal
const [modalDataState, setModalDataState] = useState(DEFAULT_VARIANT_STATE);
const [submitStatus, setSubmitStatus] = useState(null);
const [submitStatusMessage, setSubmitStatusMessage] = useState('');
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
@@ -52,7 +58,6 @@ export default function CurrentVariantsTable() {
const resetStates = () => {
setSubmitStatus(null);
setSubmitStatusMessage('');
resetTimer = null;
clearTimeout(resetTimer);
};
@@ -65,6 +70,8 @@ export default function CurrentVariantsTable() {
// posts all the variants at once as an array obj
const postUpdateToAPI = async (postValue: any) => {
setSubmitStatus(createInputStatus(STATUS_PROCESSING));
await postConfigUpdateToAPI({
apiPath: API_VIDEO_VARIANTS,
data: { value: postValue },
@@ -79,8 +86,7 @@ export default function CurrentVariantsTable() {
setModalProcessing(false);
handleModalCancel();
setSubmitStatus('success');
setSubmitStatusMessage('Variants updated.');
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Variants updated'));
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
if (serverStatusData.online) {
@@ -90,8 +96,7 @@ export default function CurrentVariantsTable() {
}
},
onError: (message: string) => {
setSubmitStatus('error');
setSubmitStatusMessage(message);
setSubmitStatus(createInputStatus(STATUS_ERROR, message));
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
},
});
@@ -112,7 +117,7 @@ export default function CurrentVariantsTable() {
postUpdateToAPI(postData);
};
const handleDeleteVariant = index => {
const handleDeleteVariant = (index: number) => {
const postData = [...videoQualityVariants];
postData.splice(index, 1);
postUpdateToAPI(postData);
@@ -125,9 +130,6 @@ export default function CurrentVariantsTable() {
});
};
const { icon: newStatusIcon = null, message: newStatusMessage = '' } =
SUCCESS_STATES[submitStatus] || {};
const videoQualityColumns: ColumnsType<VideoVariant> = [
{
title: 'Video bitrate',
@@ -176,12 +178,6 @@ export default function CurrentVariantsTable() {
},
];
const statusMessage = (
<div className={`status-message ${submitStatus || ''}`}>
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
</div>
);
const videoQualityVariantData = videoQualityVariants.map((variant, index) => ({
key: index + 1,
...variant,
@@ -189,9 +185,11 @@ export default function CurrentVariantsTable() {
return (
<>
<Title level={3}>Stream output</Title>
<Title level={3} className="section-title">
Stream output
</Title>
{statusMessage}
<FormStatusIndicator status={submitStatus} />
<Table
className="variants-table"
@@ -207,10 +205,11 @@ export default function CurrentVariantsTable() {
onOk={handleModalOk}
onCancel={handleModalCancel}
confirmLoading={modalProcessing}
width={900}
>
<VideoVariantForm dataState={{ ...modalDataState }} onUpdateField={handleUpdateField} />
{statusMessage}
<FormStatusIndicator status={submitStatus} />
</Modal>
<br />
<Button

View File

@@ -49,7 +49,7 @@ export default function MainLayout(props) {
const { Header, Footer, Content, Sider } = Layout;
const { SubMenu } = Menu;
const [upgradeVersion, setUpgradeVersion] = useState(null);
const [upgradeVersion, setUpgradeVersion] = useState('');
const checkForUpgrade = async () => {
try {
const result = await upgradeVersionAvailable(versionNumber);
@@ -80,7 +80,8 @@ export default function MainLayout(props) {
});
const upgradeMenuItemStyle = upgradeVersion ? 'block' : 'none';
const upgradeVersionString = upgradeVersion || '';
const upgradeVersionString = `${upgradeVersion}` || '';
const upgradeMessage = `Upgrade to v${upgradeVersionString}`;
const clearAlertMessage = () => {
alertMessage.setMessage(null);
@@ -123,10 +124,10 @@ export default function MainLayout(props) {
<Sider width={240} className="side-nav">
<Menu
theme="dark"
defaultSelectedKeys={[route.substring(1) || 'home']}
defaultOpenKeys={['current-stream-menu', 'utilities-menu', 'configuration']}
mode="inline"
className="menu-container"
>
<h1 className="owncast-title">
<span className="logo-container">
@@ -150,13 +151,6 @@ export default function MainLayout(props) {
<Menu.Item key="config-public-details">
<Link href="/config-public-details">General</Link>
</Menu.Item>
<Menu.Item key="config-social-items">
<Link href="/config-social-items">Social Links</Link>
</Menu.Item>
<Menu.Item key="config-page-content">
<Link href="/config-page-content">Page Content</Link>
</Menu.Item>
<Menu.Item key="config-server-details">
<Link href="/config-server-details">Server Setup</Link>
@@ -177,9 +171,7 @@ export default function MainLayout(props) {
<Link href="/logs">Logs</Link>
</Menu.Item>
<Menu.Item key="upgrade" style={{ display: upgradeMenuItemStyle }}>
<Link href="/upgrade">
<a>Upgrade to v{upgradeVersionString}</a>
</Link>
<Link href="/upgrade">{upgradeMessage}</Link>
</Menu.Item>
</SubMenu>
<SubMenu key="integrations-menu" icon={<ExperimentOutlined />} title="Integrations">