- create default values for starter fields
- add starter api urls - try and add TS types for initial components and objects - cleanup status indicator on layout header - create custom textfield for config form editing
This commit is contained in:
parent
f63fe9ea7b
commit
f0e5bbae1f
@ -4,4 +4,30 @@ export const DEFAULT_NAME = 'Owncast User';
|
|||||||
export const DEFAULT_TITLE = 'Owncast Server';
|
export const DEFAULT_TITLE = 'Owncast Server';
|
||||||
export const DEFAULT_SUMMARY = '';
|
export const DEFAULT_SUMMARY = '';
|
||||||
|
|
||||||
export const TEXT_MAXLENGTH = 255;
|
export const TEXT_MAXLENGTH = 255;
|
||||||
|
|
||||||
|
|
||||||
|
// Creating this so that it'll be easier to change values in one place, rather than looking for places to change it in a sea of JSX.
|
||||||
|
// key is the input's `fieldName`
|
||||||
|
//serversummary
|
||||||
|
export const TEXTFIELD_DEFAULTS = {
|
||||||
|
name: {
|
||||||
|
apiPath: '/name',
|
||||||
|
defaultValue: DEFAULT_NAME,
|
||||||
|
maxLength: TEXT_MAXLENGTH,
|
||||||
|
placeholder: DEFAULT_NAME,
|
||||||
|
configPath: 'instanceDetails',
|
||||||
|
label: 'Your name',
|
||||||
|
tip: 'This is your name that shows up on things and stuff.',
|
||||||
|
},
|
||||||
|
|
||||||
|
summary: {
|
||||||
|
apiPath: '/summary',
|
||||||
|
defaultValue: DEFAULT_NAME,
|
||||||
|
maxLength: TEXT_MAXLENGTH,
|
||||||
|
placeholder: DEFAULT_NAME,
|
||||||
|
configPath: 'instanceDetails',
|
||||||
|
label: 'Summary',
|
||||||
|
tip: 'A brief blurb about what your stream is about.',
|
||||||
|
}
|
||||||
|
}
|
@ -16,49 +16,98 @@ update vals to state, andthru api.
|
|||||||
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React, { useState, useContext } from 'react';
|
||||||
import { Form, Input } from 'antd';
|
import { Form, Input, Tooltip } from 'antd';
|
||||||
|
import { FormItemProps } from 'antd/es/form';
|
||||||
|
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
interface TextFieldProps {
|
import { TEXTFIELD_DEFAULTS, TEXT_MAXLENGTH } from './defaults';
|
||||||
onSubmit: (value: string) => void;
|
|
||||||
label: string;
|
|
||||||
defaultValue: string;
|
|
||||||
value: string;
|
|
||||||
helpInfo: string;
|
|
||||||
maxLength: number;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// // do i need this?
|
import { TextFieldProps } from '../../../types/config-section';
|
||||||
// export const initialProps: TextFieldProps = {
|
import { fetchData, SERVER_CONFIG_UPDATE_URL } from '../../../utils/apis';
|
||||||
// }
|
import { ServerStatusContext } from '../../../utils/server-status-context';
|
||||||
|
|
||||||
export const TEXTFIELD_TYPE_TEXT = 'default';
|
export const TEXTFIELD_TYPE_TEXT = 'default';
|
||||||
export const TEXTFIELD_TYPE_PASSWORD = 'password'; //Input.Password
|
export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password
|
||||||
export const TEXTFIELD_TYPE_NUMBER = 'numeric';
|
export const TEXTFIELD_TYPE_NUMBER = 'numeric';
|
||||||
|
|
||||||
export default function TextField(props: TextFieldProps) {
|
export default function TextField(props: TextFieldProps) {
|
||||||
|
const [submitStatus, setSubmitStatus] = useState<FormItemProps['validateStatus']>('');
|
||||||
|
const [submitStatusMessage, setSubmitStatusMessage] = useState('');
|
||||||
|
const serverStatusData = useContext(ServerStatusContext);
|
||||||
|
const { setConfigField } = serverStatusData || {};
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
label,
|
fieldName,
|
||||||
defaultValue,
|
|
||||||
value,
|
|
||||||
onSubmit,
|
|
||||||
helpInfo,
|
|
||||||
maxLength,
|
|
||||||
type,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
const {
|
||||||
|
apiPath = '',
|
||||||
|
defaultValue = '', // if empty
|
||||||
|
configPath = '',
|
||||||
|
maxLength = TEXT_MAXLENGTH,
|
||||||
|
placeholder = '',
|
||||||
|
label = '',
|
||||||
|
tip = '',
|
||||||
|
} = TEXTFIELD_DEFAULTS[fieldName] || {};
|
||||||
|
|
||||||
|
const postUpdateToAPI = async (postValue: any) => {
|
||||||
|
setSubmitStatus('validating');
|
||||||
|
const result = await fetchData(`${SERVER_CONFIG_UPDATE_URL}${apiPath}`, {
|
||||||
|
data: { value: postValue },
|
||||||
|
method: 'POST',
|
||||||
|
auth: true,
|
||||||
|
});
|
||||||
|
if (result.success) {
|
||||||
|
setConfigField({ fieldName, value: postValue, path: configPath });
|
||||||
|
setSubmitStatus('success');
|
||||||
|
} else {
|
||||||
|
setSubmitStatus('warning');
|
||||||
|
setSubmitStatusMessage(`There was an error: ${result.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnter = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
const newValue = event.target.value;
|
||||||
|
if (newValue !== '') {
|
||||||
|
postUpdateToAPI(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
const newValue = event.target.value;
|
||||||
|
if (newValue !== '') {
|
||||||
|
console.log("blur post..", newValue)
|
||||||
|
postUpdateToAPI(newValue);
|
||||||
|
} else {
|
||||||
|
// event.target.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="textfield">
|
<div className="textfield">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={label}
|
label={label}
|
||||||
|
name={fieldName}
|
||||||
hasFeedback
|
hasFeedback
|
||||||
validateStatus="error"
|
validateStatus={submitStatus}
|
||||||
help="Should be combination of numbers & alphabets"
|
help={submitStatusMessage}
|
||||||
>
|
>
|
||||||
<Input placeholder="Owncast" value={value} />
|
<Input
|
||||||
|
className="field"
|
||||||
|
allowClear
|
||||||
|
placeholder={placeholder}
|
||||||
|
maxLength={maxLength}
|
||||||
|
onPressEnter={handleEnter}
|
||||||
|
// onBlur={handleBlur}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<div className="info">
|
||||||
|
<Tooltip title={tip}>
|
||||||
|
<InfoCircleOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,41 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import { Typography, Input, Form } from 'antd';
|
import { Typography, Form, Input } from 'antd';
|
||||||
|
|
||||||
import TextField from './form-textfield';
|
import TextField from './form-textfield';
|
||||||
|
|
||||||
|
|
||||||
import { ServerStatusContext } from '../../../utils/server-status-context';
|
import { ServerStatusContext } from '../../../utils/server-status-context';
|
||||||
|
|
||||||
|
import { UpdateArgs } from '../../../types/config-section';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
export default function PublicFacingDetails() {
|
export default function PublicFacingDetails() {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const serverStatusData = useContext(ServerStatusContext);
|
const serverStatusData = useContext(ServerStatusContext);
|
||||||
const { serverConfig, setConfigField } = serverStatusData || {};
|
const { serverConfig, setConfigField } = serverStatusData || {};
|
||||||
|
|
||||||
const { instanceDetails = {}, } = serverConfig;
|
const { instanceDetails = {}, } = serverConfig;
|
||||||
|
|
||||||
const { name, summary, title } = instanceDetails;
|
const { name, summary, title } = instanceDetails;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.setFieldsValue({...instanceDetails});
|
||||||
|
}, [instanceDetails]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title level={2}>Edit your public facing instance details</Title>
|
<Title level={2}>Edit your public facing instance details</Title>
|
||||||
|
|
||||||
<div className="config-public-details-container">
|
<div className="config-public-details-container">
|
||||||
<div className="text-fields">
|
<div className="text-fields">
|
||||||
<Form name="text-fields" labelCol={{ span: 8 }} wrapperCol={{ span: 16 }}>
|
<Form
|
||||||
<TextField
|
form={form}
|
||||||
label="name"
|
layout="vertical"
|
||||||
value={name}
|
>
|
||||||
/>
|
<TextField fieldName="name" />
|
||||||
|
<TextField fieldName="summary" />
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
<div className="misc-optionals">
|
<div className="misc-optionals">
|
||||||
|
@ -41,15 +41,29 @@ export default function MainLayout(props) {
|
|||||||
const { Header, Footer, Content, Sider } = Layout;
|
const { Header, Footer, Content, Sider } = Layout;
|
||||||
const { SubMenu } = Menu;
|
const { SubMenu } = Menu;
|
||||||
|
|
||||||
|
// status indicator items
|
||||||
const streamDurationString = online ? parseSecondsToDurationString(differenceInSeconds(new Date(), new Date(broadcaster.time))) : "";
|
const streamDurationString = online ? parseSecondsToDurationString(differenceInSeconds(new Date(), new Date(broadcaster.time))) : "";
|
||||||
|
const currentThumbnail = online ? (
|
||||||
const content = (
|
<img src="/thumbnail.jpg" className={adminStyles.onlineCurrentThumb} alt="current thumbnail" />
|
||||||
<div>
|
) : null;
|
||||||
<img src="/thumbnail.jpg" width="200px" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
const statusIcon = online ? <PlayCircleFilled /> : <MinusSquareFilled />;
|
const statusIcon = online ? <PlayCircleFilled /> : <MinusSquareFilled />;
|
||||||
const statusMessage = online ? `Online ${streamDurationString}` : "Offline";
|
const statusMessage = online ? `Online ${streamDurationString}` : "Offline";
|
||||||
|
const statusIndicator = (
|
||||||
|
<div className={adminStyles.statusIndicatorContainer}>
|
||||||
|
<span className={adminStyles.statusLabel}>{statusMessage}</span>
|
||||||
|
<span className={adminStyles.statusIcon}>{statusIcon}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const statusIndicatorWithThumb = online ? (
|
||||||
|
<Popover
|
||||||
|
content={currentThumbnail}
|
||||||
|
title="Thumbnail"
|
||||||
|
trigger="hover"
|
||||||
|
>
|
||||||
|
{statusIndicator}
|
||||||
|
</Popover>
|
||||||
|
) : statusIndicator;
|
||||||
|
// ///////////////
|
||||||
|
|
||||||
const [upgradeVersion, setUpgradeVersion] = useState(null);
|
const [upgradeVersion, setUpgradeVersion] = useState(null);
|
||||||
const checkForUpgrade = async () => {
|
const checkForUpgrade = async () => {
|
||||||
@ -176,12 +190,7 @@ export default function MainLayout(props) {
|
|||||||
|
|
||||||
<Layout className={adminStyles.layoutMain}>
|
<Layout className={adminStyles.layoutMain}>
|
||||||
<Header className={adminStyles.header}>
|
<Header className={adminStyles.header}>
|
||||||
<Popover content={content} title="Thumbnail" trigger="hover">
|
{statusIndicatorWithThumb}
|
||||||
<div className={adminStyles.statusIndicatorContainer}>
|
|
||||||
<span className={adminStyles.statusLabel}>{statusMessage}</span>
|
|
||||||
<span className={adminStyles.statusIcon}>{statusIcon}</span>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</Header>
|
</Header>
|
||||||
<Content className={adminStyles.contentMain}>{children}</Content>
|
<Content className={adminStyles.contentMain}>{children}</Content>
|
||||||
|
|
||||||
|
@ -4,4 +4,24 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.text-fields {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.textfield {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.field {
|
||||||
|
width: 20rem;
|
||||||
|
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
transform: translateY(.35rem);
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,3 +86,7 @@
|
|||||||
.configSection {
|
.configSection {
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.onlineCurrentThumb {
|
||||||
|
width: 12.5rem;
|
||||||
|
}
|
||||||
|
14
web/types/config-section.ts
Normal file
14
web/types/config-section.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// TS types for elements on the Config pages
|
||||||
|
|
||||||
|
export interface TextFieldProps {
|
||||||
|
onUpdate: ({ fieldName, value }: UpdateArgs) => void;
|
||||||
|
fieldName: string;
|
||||||
|
value: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateArgs {
|
||||||
|
fieldName: string;
|
||||||
|
value: string;
|
||||||
|
path?: string;
|
||||||
|
}
|
@ -19,6 +19,9 @@ export const STREAMKEY_CHANGE = `${API_LOCATION}changekey`;
|
|||||||
// Current server config
|
// Current server config
|
||||||
export const SERVER_CONFIG = `${API_LOCATION}serverconfig`;
|
export const SERVER_CONFIG = `${API_LOCATION}serverconfig`;
|
||||||
|
|
||||||
|
// Base url to update config settings
|
||||||
|
export const SERVER_CONFIG_UPDATE_URL = `${API_LOCATION}config`;
|
||||||
|
|
||||||
// Get viewer count over time
|
// Get viewer count over time
|
||||||
export const VIEWERS_OVER_TIME = `${API_LOCATION}viewersOverTime`;
|
export const VIEWERS_OVER_TIME = `${API_LOCATION}viewersOverTime`;
|
||||||
|
|
||||||
@ -67,6 +70,12 @@ interface FetchOptions {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function fetchData(url: string, options?: FetchOptions) {
|
export async function fetchData(url: string, options?: FetchOptions) {
|
||||||
|
// TEMP
|
||||||
|
export const TEMP_UPDATER_API = LOGS_ALL;
|
||||||
|
|
||||||
|
const GITHUB_RELEASE_URL = "https://api.github.com/repos/owncast/owncast/releases/latest";
|
||||||
|
|
||||||
|
export async function fetchData(url: string, options?: object) {
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
method = 'GET',
|
method = 'GET',
|
||||||
@ -105,6 +114,7 @@ export async function fetchData(url: string, options?: FetchOptions) {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function getGithubRelease() {
|
export async function getGithubRelease() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(GITHUB_RELEASE_URL);
|
const response = await fetch(GITHUB_RELEASE_URL);
|
||||||
|
@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { STATUS, fetchData, FETCH_INTERVAL, SERVER_CONFIG } from './apis';
|
import { STATUS, fetchData, FETCH_INTERVAL, SERVER_CONFIG } from './apis';
|
||||||
|
import { UpdateArgs } from '../types/config-section';
|
||||||
|
|
||||||
export const initialServerConfigState = {
|
export const initialServerConfigState = {
|
||||||
streamKey: '',
|
streamKey: '',
|
||||||
@ -63,11 +64,20 @@ const ServerStatusProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setConfigField = ({ fieldName, value }) => {
|
const setConfigField = ({ fieldName, value, path }: UpdateArgs) => {
|
||||||
const updatedConfig = {
|
const updatedConfig = path ?
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
[path]: {
|
||||||
|
...config[path],
|
||||||
|
[fieldName]: value,
|
||||||
|
},
|
||||||
|
} :
|
||||||
|
{
|
||||||
...config,
|
...config,
|
||||||
[fieldName]: value,
|
[fieldName]: value,
|
||||||
};
|
};
|
||||||
|
|
||||||
setConfig(updatedConfig);
|
setConfig(updatedConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user