some prettifying ✨
This commit is contained in:
@@ -4,48 +4,49 @@ module.exports = {
|
|||||||
es2021: true,
|
es2021: true,
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
"plugin:react/recommended",
|
'plugin:react/recommended',
|
||||||
"airbnb",
|
'airbnb',
|
||||||
"prettier",
|
'prettier',
|
||||||
"prettier/@typescript-eslint",
|
'prettier/@typescript-eslint',
|
||||||
"prettier/react",
|
'prettier/react',
|
||||||
],
|
],
|
||||||
parser: "@typescript-eslint/parser",
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
jsx: true,
|
jsx: true,
|
||||||
},
|
},
|
||||||
ecmaVersion: 12,
|
ecmaVersion: 12,
|
||||||
sourceType: "module",
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
plugins: ["react", "@typescript-eslint"],
|
plugins: ['react', 'prettier', '@typescript-eslint'],
|
||||||
rules: {
|
rules: {
|
||||||
"react/react-in-jsx-scope": "off",
|
'prettier/prettier': 'error',
|
||||||
"react/jsx-filename-extension": [1, { extensions: [".js", ".jsx", ".tsx"] }],
|
'react/react-in-jsx-scope': 'off',
|
||||||
"react/jsx-props-no-spreading": "off",
|
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx', '.tsx'] }],
|
||||||
|
'react/jsx-props-no-spreading': 'off',
|
||||||
|
|
||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': 'error',
|
'@typescript-eslint/no-unused-vars': 'error',
|
||||||
|
|
||||||
'no-use-before-define': [0],
|
'no-use-before-define': [0],
|
||||||
'@typescript-eslint/no-use-before-define': [1],
|
'@typescript-eslint/no-use-before-define': [1],
|
||||||
|
|
||||||
"import/extensions": [
|
'import/extensions': [
|
||||||
"error",
|
'error',
|
||||||
"ignorePackages",
|
'ignorePackages',
|
||||||
{
|
{
|
||||||
"js": "never",
|
js: 'never',
|
||||||
"jsx": "never",
|
jsx: 'never',
|
||||||
"ts": "never",
|
ts: 'never',
|
||||||
"tsx": "never"
|
tsx: 'never',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
"import/resolver": {
|
'import/resolver': {
|
||||||
"node": {
|
node: {
|
||||||
"extensions": [".js", ".jsx", ".ts", ".tsx"]
|
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
{
|
{
|
||||||
|
"useTabs": false,
|
||||||
|
"printWidth": 100,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"useTabs": false
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"jsxBracketSameLine": false,
|
||||||
|
"arrowParens": "avoid"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { AppProps } from 'next/app';
|
|||||||
import ServerStatusProvider from '../utils/server-status-context';
|
import ServerStatusProvider from '../utils/server-status-context';
|
||||||
import MainLayout from './components/main-layout';
|
import MainLayout from './components/main-layout';
|
||||||
|
|
||||||
|
|
||||||
function App({ Component, pageProps }: AppProps) {
|
function App({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<ServerStatusProvider>
|
<ServerStatusProvider>
|
||||||
@@ -19,8 +18,7 @@ function App({ Component, pageProps }: AppProps) {
|
|||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
</ServerStatusProvider>
|
</ServerStatusProvider>
|
||||||
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const SUCCESS_STATES = {
|
|||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
icon: <ExclamationCircleFilled style={{ color: 'red' }} />,
|
icon: <ExclamationCircleFilled style={{ color: 'red' }} />,
|
||||||
message: 'An error occurred.',
|
message: 'An error occurred.',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,14 +39,8 @@ export const API_VIDEO_VARIANTS = '/video/streamoutputvariants';
|
|||||||
export const API_WEB_PORT = '/webserverport';
|
export const API_WEB_PORT = '/webserverport';
|
||||||
export const API_YP_SWITCH = '/directoryenabled';
|
export const API_YP_SWITCH = '/directoryenabled';
|
||||||
|
|
||||||
|
|
||||||
export async function postConfigUpdateToAPI(args: ApiPostArgs) {
|
export async function postConfigUpdateToAPI(args: ApiPostArgs) {
|
||||||
const {
|
const { apiPath, data, onSuccess, onError } = args;
|
||||||
apiPath,
|
|
||||||
data,
|
|
||||||
onSuccess,
|
|
||||||
onError,
|
|
||||||
} = args;
|
|
||||||
const result = await fetchData(`${SERVER_CONFIG_UPDATE_URL}${apiPath}`, {
|
const result = await fetchData(`${SERVER_CONFIG_UPDATE_URL}${apiPath}`, {
|
||||||
data,
|
data,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -59,7 +53,6 @@ export async function postConfigUpdateToAPI(args: ApiPostArgs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Some default props to help build out a TextField
|
// Some default props to help build out a TextField
|
||||||
export const TEXTFIELD_PROPS_USERNAME = {
|
export const TEXTFIELD_PROPS_USERNAME = {
|
||||||
apiPath: API_USERNAME,
|
apiPath: API_USERNAME,
|
||||||
@@ -95,7 +88,8 @@ export const TEXTFIELD_PROPS_LOGO = {
|
|||||||
maxLength: 255,
|
maxLength: 255,
|
||||||
placeholder: '/img/mylogo.png',
|
placeholder: '/img/mylogo.png',
|
||||||
label: 'Logo',
|
label: 'Logo',
|
||||||
tip: 'Path to your logo from website root. We recommend that you use a square image that is at least 256x256. (upload functionality coming soon)',
|
tip:
|
||||||
|
'Path to your logo from website root. We recommend that you use a square image that is at least 256x256. (upload functionality coming soon)',
|
||||||
};
|
};
|
||||||
export const TEXTFIELD_PROPS_STREAM_KEY = {
|
export const TEXTFIELD_PROPS_STREAM_KEY = {
|
||||||
apiPath: API_STREAM_KEY,
|
apiPath: API_STREAM_KEY,
|
||||||
@@ -163,17 +157,19 @@ export const FIELD_PROPS_NSFW = {
|
|||||||
apiPath: API_NSFW_SWITCH,
|
apiPath: API_NSFW_SWITCH,
|
||||||
configPath: 'instanceDetails',
|
configPath: 'instanceDetails',
|
||||||
label: 'NSFW?',
|
label: 'NSFW?',
|
||||||
tip: "Turn this ON if you plan to steam explicit or adult content. You may want to respectfully set this flag so that unexpecting eyes won't accidentally see it from the Directory.",
|
tip:
|
||||||
|
"Turn this ON if you plan to steam explicit or adult content. You may want to respectfully set this flag so that unexpecting eyes won't accidentally see it from the Directory.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FIELD_PROPS_YP = {
|
export const FIELD_PROPS_YP = {
|
||||||
apiPath: API_YP_SWITCH,
|
apiPath: API_YP_SWITCH,
|
||||||
configPath: 'yp',
|
configPath: 'yp',
|
||||||
label: 'Display in the Owncast Directory?',
|
label: 'Display in the Owncast Directory?',
|
||||||
tip: 'Turn this ON if you want to show up in the Owncast directory at https://directory.owncast.online.',
|
tip:
|
||||||
|
'Turn this ON if you want to show up in the Owncast directory at https://directory.owncast.online.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_VARIANT_STATE:VideoVariant = {
|
export const DEFAULT_VARIANT_STATE: VideoVariant = {
|
||||||
framerate: 24,
|
framerate: 24,
|
||||||
videoPassthrough: false,
|
videoPassthrough: false,
|
||||||
videoBitrate: 800,
|
videoBitrate: 800,
|
||||||
@@ -182,7 +178,7 @@ export const DEFAULT_VARIANT_STATE:VideoVariant = {
|
|||||||
cpuUsageLevel: 3,
|
cpuUsageLevel: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_SOCIAL_HANDLE:SocialHandle = {
|
export const DEFAULT_SOCIAL_HANDLE: SocialHandle = {
|
||||||
url: '',
|
url: '',
|
||||||
platform: '',
|
platform: '',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useContext, useState, useEffect } from 'react';
|
import React, { useContext, useState, useEffect } from 'react';
|
||||||
import { Typography, Slider, } from 'antd';
|
import { Typography, Slider } from 'antd';
|
||||||
import { ServerStatusContext } from '../../../utils/server-status-context';
|
import { ServerStatusContext } from '../../../utils/server-status-context';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
@@ -12,8 +12,7 @@ const SLIDER_MARKS = {
|
|||||||
5: 'highest',
|
5: 'highest',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default function CPUUsageSelector({ defaultValue, onChange }) {
|
||||||
export default function CPUUsageSelector({defaultValue, onChange}) {
|
|
||||||
const [selectedOption, setSelectedOption] = useState(null);
|
const [selectedOption, setSelectedOption] = useState(null);
|
||||||
|
|
||||||
const serverStatusData = useContext(ServerStatusContext);
|
const serverStatusData = useContext(ServerStatusContext);
|
||||||
@@ -27,21 +26,20 @@ export default function CPUUsageSelector({defaultValue, onChange}) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedOption(defaultValue);
|
setSelectedOption(defaultValue);
|
||||||
}, [videoSettings]);
|
}, [videoSettings]);
|
||||||
|
|
||||||
const handleChange = value => {
|
const handleChange = value => {
|
||||||
setSelectedOption(value);
|
setSelectedOption(value);
|
||||||
onChange(value);
|
onChange(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-container config-video-segements-conatiner">
|
<div className="module-container config-video-segements-conatiner">
|
||||||
<Title level={3}>CPU Usage</Title>
|
<Title level={3}>CPU Usage</Title>
|
||||||
<p>
|
<p>There are trade-offs when considering CPU usage blah blah more wording here.</p>
|
||||||
There are trade-offs when considering CPU usage blah blah more wording here.
|
<br />
|
||||||
</p>
|
<br />
|
||||||
<br /><br />
|
|
||||||
<div className="segment-slider">
|
<div className="segment-slider">
|
||||||
<Slider
|
<Slider
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
min={1}
|
min={1}
|
||||||
max={Object.keys(SLIDER_MARKS).length}
|
max={Object.keys(SLIDER_MARKS).length}
|
||||||
@@ -52,4 +50,4 @@ export default function CPUUsageSelector({defaultValue, onChange}) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export default function EditYPDetails() {
|
|||||||
const { nsfw } = instanceDetails;
|
const { nsfw } = instanceDetails;
|
||||||
const { enabled, instanceUrl } = yp;
|
const { enabled, instanceUrl } = yp;
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFormDataValues({
|
setFormDataValues({
|
||||||
...yp,
|
...yp,
|
||||||
@@ -35,10 +34,21 @@ export default function EditYPDetails() {
|
|||||||
return (
|
return (
|
||||||
<div className="config-directory-details-form">
|
<div className="config-directory-details-form">
|
||||||
<Title level={3}>Owncast Directory Settings</Title>
|
<Title level={3}>Owncast Directory Settings</Title>
|
||||||
|
|
||||||
<p>Would you like to appear in the <a href="https://directory.owncast.online" target="_blank" rel="noreferrer"><strong>Owncast Directory</strong></a>?</p>
|
|
||||||
|
|
||||||
<p style={{ backgroundColor: 'black', fontSize: '.75rem', padding: '5px' }}><em>NOTE: You will need to have a URL specified in the <code>Instance URL</code> field to be able to use this.</em></p>
|
<p>
|
||||||
|
Would you like to appear in the{' '}
|
||||||
|
<a href="https://directory.owncast.online" target="_blank" rel="noreferrer">
|
||||||
|
<strong>Owncast Directory</strong>
|
||||||
|
</a>
|
||||||
|
?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style={{ backgroundColor: 'black', fontSize: '.75rem', padding: '5px' }}>
|
||||||
|
<em>
|
||||||
|
NOTE: You will need to have a URL specified in the <code>Instance URL</code> field to be
|
||||||
|
able to use this.
|
||||||
|
</em>
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="config-yp-container">
|
<div className="config-yp-container">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
@@ -53,9 +63,7 @@ export default function EditYPDetails() {
|
|||||||
checked={formDataValues.nsfw}
|
checked={formDataValues.nsfw}
|
||||||
disabled={!hasInstanceUrl}
|
disabled={!hasInstanceUrl}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import React, { useState, useContext, useEffect } from 'react';
|
import React, { useState, useContext, useEffect } from 'react';
|
||||||
import TextFieldWithSubmit, { TEXTFIELD_TYPE_TEXTAREA, TEXTFIELD_TYPE_URL } from './form-textfield-with-submit';
|
import TextFieldWithSubmit, {
|
||||||
|
TEXTFIELD_TYPE_TEXTAREA,
|
||||||
|
TEXTFIELD_TYPE_URL,
|
||||||
|
} from './form-textfield-with-submit';
|
||||||
|
|
||||||
import { ServerStatusContext } from '../../../utils/server-status-context';
|
import { ServerStatusContext } from '../../../utils/server-status-context';
|
||||||
import { postConfigUpdateToAPI, TEXTFIELD_PROPS_USERNAME, TEXTFIELD_PROPS_INSTANCE_URL, TEXTFIELD_PROPS_SERVER_TITLE, TEXTFIELD_PROPS_STREAM_TITLE, TEXTFIELD_PROPS_SERVER_SUMMARY, TEXTFIELD_PROPS_LOGO, API_YP_SWITCH } from './constants';
|
import {
|
||||||
|
postConfigUpdateToAPI,
|
||||||
|
TEXTFIELD_PROPS_USERNAME,
|
||||||
|
TEXTFIELD_PROPS_INSTANCE_URL,
|
||||||
|
TEXTFIELD_PROPS_SERVER_TITLE,
|
||||||
|
TEXTFIELD_PROPS_STREAM_TITLE,
|
||||||
|
TEXTFIELD_PROPS_SERVER_SUMMARY,
|
||||||
|
TEXTFIELD_PROPS_LOGO,
|
||||||
|
API_YP_SWITCH,
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
import configStyles from '../../../styles/config-pages.module.scss';
|
import configStyles from '../../../styles/config-pages.module.scss';
|
||||||
import { UpdateArgs } from '../../../types/config-section';
|
import { UpdateArgs } from '../../../types/config-section';
|
||||||
@@ -35,16 +47,16 @@ export default function EditInstanceDetails() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleFieldChange = ({ fieldName, value }: UpdateArgs) => {
|
const handleFieldChange = ({ fieldName, value }: UpdateArgs) => {
|
||||||
setFormDataValues({
|
setFormDataValues({
|
||||||
...formDataValues,
|
...formDataValues,
|
||||||
[fieldName]: value,
|
[fieldName]: value,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={configStyles.publicDetailsContainer}>
|
<div className={configStyles.publicDetailsContainer}>
|
||||||
<div className={configStyles.textFieldsSection}>
|
<div className={configStyles.textFieldsSection}>
|
||||||
<TextFieldWithSubmit
|
<TextFieldWithSubmit
|
||||||
@@ -56,7 +68,7 @@ export default function EditInstanceDetails() {
|
|||||||
onChange={handleFieldChange}
|
onChange={handleFieldChange}
|
||||||
onSubmit={handleSubmitInstanceUrl}
|
onSubmit={handleSubmitInstanceUrl}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextFieldWithSubmit
|
<TextFieldWithSubmit
|
||||||
fieldName="title"
|
fieldName="title"
|
||||||
{...TEXTFIELD_PROPS_SERVER_TITLE}
|
{...TEXTFIELD_PROPS_SERVER_TITLE}
|
||||||
@@ -94,8 +106,6 @@ export default function EditInstanceDetails() {
|
|||||||
onChange={handleFieldChange}
|
onChange={handleFieldChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import { TEXTFIELD_TYPE_NUMBER, TEXTFIELD_TYPE_PASSWORD } from './form-textfield
|
|||||||
import TextFieldWithSubmit from './form-textfield-with-submit';
|
import TextFieldWithSubmit from './form-textfield-with-submit';
|
||||||
|
|
||||||
import { ServerStatusContext } from '../../../utils/server-status-context';
|
import { ServerStatusContext } from '../../../utils/server-status-context';
|
||||||
import { TEXTFIELD_PROPS_FFMPEG, TEXTFIELD_PROPS_RTMP_PORT, TEXTFIELD_PROPS_STREAM_KEY, TEXTFIELD_PROPS_WEB_PORT, } from './constants';
|
import {
|
||||||
|
TEXTFIELD_PROPS_FFMPEG,
|
||||||
|
TEXTFIELD_PROPS_RTMP_PORT,
|
||||||
|
TEXTFIELD_PROPS_STREAM_KEY,
|
||||||
|
TEXTFIELD_PROPS_WEB_PORT,
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
import configStyles from '../../../styles/config-pages.module.scss';
|
import configStyles from '../../../styles/config-pages.module.scss';
|
||||||
import { UpdateArgs } from '../../../types/config-section';
|
import { UpdateArgs } from '../../../types/config-section';
|
||||||
@@ -18,13 +23,16 @@ export default function EditInstanceDetails() {
|
|||||||
|
|
||||||
const { streamKey, ffmpegPath, rtmpServerPort, webServerPort } = serverConfig;
|
const { streamKey, ffmpegPath, rtmpServerPort, webServerPort } = serverConfig;
|
||||||
|
|
||||||
const [copyIsVisible, setCopyVisible] = useState(false);
|
const [copyIsVisible, setCopyVisible] = useState(false);
|
||||||
|
|
||||||
const COPY_TOOLTIP_TIMEOUT = 3000;
|
const COPY_TOOLTIP_TIMEOUT = 3000;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFormDataValues({
|
setFormDataValues({
|
||||||
streamKey, ffmpegPath, rtmpServerPort, webServerPort
|
streamKey,
|
||||||
|
ffmpegPath,
|
||||||
|
rtmpServerPort,
|
||||||
|
webServerPort,
|
||||||
});
|
});
|
||||||
}, [serverConfig]);
|
}, [serverConfig]);
|
||||||
|
|
||||||
@@ -37,26 +45,25 @@ export default function EditInstanceDetails() {
|
|||||||
...formDataValues,
|
...formDataValues,
|
||||||
[fieldName]: value,
|
[fieldName]: value,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
function generateStreamKey () {
|
function generateStreamKey() {
|
||||||
let key = '';
|
let key = '';
|
||||||
for (let i = 0; i < 3; i+=1) {
|
for (let i = 0; i < 3; i += 1) {
|
||||||
key += Math.random().toString(36).substring(2);
|
key += Math.random().toString(36).substring(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFieldChange({ fieldName: 'streamKey', value: key });
|
handleFieldChange({ fieldName: 'streamKey', value: key });
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyStreamKey () {
|
function copyStreamKey() {
|
||||||
navigator.clipboard.writeText(formDataValues.streamKey)
|
navigator.clipboard.writeText(formDataValues.streamKey).then(() => {
|
||||||
.then(() => {
|
setCopyVisible(true);
|
||||||
setCopyVisible(true);
|
setTimeout(() => setCopyVisible(false), COPY_TOOLTIP_TIMEOUT);
|
||||||
setTimeout(() => setCopyVisible(false), COPY_TOOLTIP_TIMEOUT);
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={configStyles.publicDetailsContainer}>
|
<div className={configStyles.publicDetailsContainer}>
|
||||||
<div className={configStyles.textFieldsSection}>
|
<div className={configStyles.textFieldsSection}>
|
||||||
<TextFieldWithSubmit
|
<TextFieldWithSubmit
|
||||||
@@ -68,25 +75,14 @@ export default function EditInstanceDetails() {
|
|||||||
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,
|
Save this key somewhere safe, you will need it to stream or login to the admin
|
||||||
you will need it to stream or login to the admin dashboard!
|
dashboard!
|
||||||
</span>
|
</span>
|
||||||
<Tooltip className="copy-tooltip"
|
<Tooltip className="copy-tooltip" title="Copied!" trigger="" visible={copyIsVisible}>
|
||||||
title="Copied!"
|
<Button type="primary" icon={<CopyOutlined />} size="small" onClick={copyStreamKey} />
|
||||||
trigger=""
|
|
||||||
visible={copyIsVisible}>
|
|
||||||
<Button type="primary"
|
|
||||||
icon={<CopyOutlined />}
|
|
||||||
size="small"
|
|
||||||
onClick={copyStreamKey}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Button type="primary"
|
<Button type="primary" icon={<RedoOutlined />} size="small" onClick={generateStreamKey} />
|
||||||
icon={<RedoOutlined />}
|
|
||||||
size="small"
|
|
||||||
onClick={generateStreamKey}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<TextFieldWithSubmit
|
<TextFieldWithSubmit
|
||||||
fieldName="ffmpegPath"
|
fieldName="ffmpegPath"
|
||||||
@@ -112,8 +108,6 @@ export default function EditInstanceDetails() {
|
|||||||
onChange={handleFieldChange}
|
onChange={handleFieldChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ import { DeleteOutlined } from '@ant-design/icons';
|
|||||||
import SocialDropdown from './social-icons-dropdown';
|
import SocialDropdown from './social-icons-dropdown';
|
||||||
import { fetchData, NEXT_PUBLIC_API_HOST, SOCIAL_PLATFORMS_LIST } from '../../../utils/apis';
|
import { fetchData, NEXT_PUBLIC_API_HOST, SOCIAL_PLATFORMS_LIST } from '../../../utils/apis';
|
||||||
import { ServerStatusContext } from '../../../utils/server-status-context';
|
import { ServerStatusContext } from '../../../utils/server-status-context';
|
||||||
import { API_SOCIAL_HANDLES, postConfigUpdateToAPI, RESET_TIMEOUT, SUCCESS_STATES, DEFAULT_SOCIAL_HANDLE, OTHER_SOCIAL_HANDLE_OPTION } from './constants';
|
import {
|
||||||
|
API_SOCIAL_HANDLES,
|
||||||
|
postConfigUpdateToAPI,
|
||||||
|
RESET_TIMEOUT,
|
||||||
|
SUCCESS_STATES,
|
||||||
|
DEFAULT_SOCIAL_HANDLE,
|
||||||
|
OTHER_SOCIAL_HANDLE_OPTION,
|
||||||
|
} from './constants';
|
||||||
import { SocialHandle } from '../../../types/config-section';
|
import { SocialHandle } from '../../../types/config-section';
|
||||||
import { isValidUrl } from '../../../utils/urls';
|
import { isValidUrl } from '../../../utils/urls';
|
||||||
|
|
||||||
@@ -21,7 +28,7 @@ export default function EditSocialLinks() {
|
|||||||
const [displayOther, setDisplayOther] = useState(false);
|
const [displayOther, setDisplayOther] = useState(false);
|
||||||
const [modalProcessing, setModalProcessing] = useState(false);
|
const [modalProcessing, setModalProcessing] = useState(false);
|
||||||
const [editId, setEditId] = useState(-1);
|
const [editId, setEditId] = useState(-1);
|
||||||
|
|
||||||
// current data inside modal
|
// current data inside modal
|
||||||
const [modalDataState, setModalDataState] = useState(DEFAULT_SOCIAL_HANDLE);
|
const [modalDataState, setModalDataState] = useState(DEFAULT_SOCIAL_HANDLE);
|
||||||
|
|
||||||
@@ -44,15 +51,15 @@ export default function EditSocialLinks() {
|
|||||||
...result[item],
|
...result[item],
|
||||||
}));
|
}));
|
||||||
setAvailableIconsList(list);
|
setAvailableIconsList(list);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error);
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedOther = modalDataState.platform !== '' && !availableIconsList.find(item => item.key === modalDataState.platform);
|
const selectedOther =
|
||||||
|
modalDataState.platform !== '' &&
|
||||||
|
!availableIconsList.find(item => item.key === modalDataState.platform);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getAvailableIcons();
|
getAvailableIcons();
|
||||||
@@ -64,7 +71,6 @@ export default function EditSocialLinks() {
|
|||||||
}
|
}
|
||||||
}, [instanceDetails]);
|
}, [instanceDetails]);
|
||||||
|
|
||||||
|
|
||||||
const resetStates = () => {
|
const resetStates = () => {
|
||||||
setSubmitStatus(null);
|
setSubmitStatus(null);
|
||||||
setSubmitStatusMessage('');
|
setSubmitStatusMessage('');
|
||||||
@@ -76,7 +82,7 @@ export default function EditSocialLinks() {
|
|||||||
setEditId(-1);
|
setEditId(-1);
|
||||||
setDisplayOther(false);
|
setDisplayOther(false);
|
||||||
setModalProcessing(false);
|
setModalProcessing(false);
|
||||||
setModalDataState({...DEFAULT_SOCIAL_HANDLE});
|
setModalDataState({ ...DEFAULT_SOCIAL_HANDLE });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleModalCancel = () => {
|
const handleModalCancel = () => {
|
||||||
@@ -106,7 +112,6 @@ export default function EditSocialLinks() {
|
|||||||
const { value } = event.target;
|
const { value } = event.target;
|
||||||
updateModalState('url', value);
|
updateModalState('url', value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// posts all the variants at once as an array obj
|
// posts all the variants at once as an array obj
|
||||||
const postUpdateToAPI = async (postValue: any) => {
|
const postUpdateToAPI = async (postValue: any) => {
|
||||||
@@ -114,7 +119,11 @@ export default function EditSocialLinks() {
|
|||||||
apiPath: API_SOCIAL_HANDLES,
|
apiPath: API_SOCIAL_HANDLES,
|
||||||
data: { value: postValue },
|
data: { value: postValue },
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setFieldInConfigState({ fieldName: 'socialHandles', value: postValue, path: 'instanceDetails' });
|
setFieldInConfigState({
|
||||||
|
fieldName: 'socialHandles',
|
||||||
|
value: postValue,
|
||||||
|
path: 'instanceDetails',
|
||||||
|
});
|
||||||
|
|
||||||
// close modal
|
// close modal
|
||||||
setModalProcessing(false);
|
setModalProcessing(false);
|
||||||
@@ -132,15 +141,12 @@ export default function EditSocialLinks() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// on Ok, send all of dataState to api
|
// on Ok, send all of dataState to api
|
||||||
// show loading
|
// show loading
|
||||||
// close modal when api is done
|
// close modal when api is done
|
||||||
const handleModalOk = () => {
|
const handleModalOk = () => {
|
||||||
setModalProcessing(true);
|
setModalProcessing(true);
|
||||||
const postData = currentSocialHandles.length ? [
|
const postData = currentSocialHandles.length ? [...currentSocialHandles] : [];
|
||||||
...currentSocialHandles,
|
|
||||||
]: [];
|
|
||||||
if (editId === -1) {
|
if (editId === -1) {
|
||||||
postData.push(modalDataState);
|
postData.push(modalDataState);
|
||||||
} else {
|
} else {
|
||||||
@@ -150,23 +156,21 @@ export default function EditSocialLinks() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteItem = index => {
|
const handleDeleteItem = index => {
|
||||||
const postData = [
|
const postData = [...currentSocialHandles];
|
||||||
...currentSocialHandles,
|
|
||||||
];
|
|
||||||
postData.splice(index, 1);
|
postData.splice(index, 1);
|
||||||
postUpdateToAPI(postData);
|
postUpdateToAPI(postData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const socialHandlesColumns: ColumnsType<SocialHandle> = [
|
const socialHandlesColumns: ColumnsType<SocialHandle> = [
|
||||||
{
|
{
|
||||||
title: "#",
|
title: '#',
|
||||||
dataIndex: "key",
|
dataIndex: 'key',
|
||||||
key: "key"
|
key: 'key',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Platform",
|
title: 'Platform',
|
||||||
dataIndex: "platform",
|
dataIndex: 'platform',
|
||||||
key: "platform",
|
key: 'platform',
|
||||||
render: (platform: string) => {
|
render: (platform: string) => {
|
||||||
const platformInfo = availableIconsList.find(item => item.key === platform);
|
const platformInfo = availableIconsList.find(item => item.key === platform);
|
||||||
if (!platformInfo) {
|
if (!platformInfo) {
|
||||||
@@ -185,9 +189,9 @@ export default function EditSocialLinks() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "Url Link",
|
title: 'Url Link',
|
||||||
dataIndex: "url",
|
dataIndex: 'url',
|
||||||
key: "url",
|
key: 'url',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
@@ -196,28 +200,31 @@ export default function EditSocialLinks() {
|
|||||||
render: (data, record, index) => {
|
render: (data, record, index) => {
|
||||||
return (
|
return (
|
||||||
<span className="actions">
|
<span className="actions">
|
||||||
<Button type="primary" size="small" onClick={() => {
|
<Button
|
||||||
setEditId(index);
|
type="primary"
|
||||||
setModalDataState({...currentSocialHandles[index]});
|
size="small"
|
||||||
setDisplayModal(true);
|
onClick={() => {
|
||||||
}}>
|
setEditId(index);
|
||||||
|
setModalDataState({ ...currentSocialHandles[index] });
|
||||||
|
setDisplayModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="delete-button"
|
className="delete-button"
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => handleDeleteItem(index)}
|
onClick={() => handleDeleteItem(index)}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)},
|
);
|
||||||
},
|
},
|
||||||
];
|
},
|
||||||
|
];
|
||||||
const {
|
|
||||||
icon: newStatusIcon = null,
|
const { icon: newStatusIcon = null, message: newStatusMessage = '' } =
|
||||||
message: newStatusMessage = '',
|
SUCCESS_STATES[submitStatus] || {};
|
||||||
} = SUCCESS_STATES[submitStatus] || {};
|
|
||||||
const statusMessage = (
|
const statusMessage = (
|
||||||
<div className={`status-message ${submitStatus || ''}`}>
|
<div className={`status-message ${submitStatus || ''}`}>
|
||||||
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
|
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
|
||||||
@@ -225,9 +232,8 @@ export default function EditSocialLinks() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const okButtonProps = {
|
const okButtonProps = {
|
||||||
disabled: !isValidUrl(modalDataState.url)
|
disabled: !isValidUrl(modalDataState.url),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={configStyles.socialLinksEditor}>
|
<div className={configStyles.socialLinksEditor}>
|
||||||
@@ -258,21 +264,17 @@ export default function EditSocialLinks() {
|
|||||||
selectedOption={selectedOther ? OTHER_SOCIAL_HANDLE_OPTION : modalDataState.platform}
|
selectedOption={selectedOther ? OTHER_SOCIAL_HANDLE_OPTION : modalDataState.platform}
|
||||||
onSelected={handleDropdownSelect}
|
onSelected={handleDropdownSelect}
|
||||||
/>
|
/>
|
||||||
{
|
{displayOther ? (
|
||||||
displayOther
|
<>
|
||||||
? (
|
<Input
|
||||||
<>
|
placeholder="Other"
|
||||||
<Input
|
defaultValue={modalDataState.platform}
|
||||||
placeholder="Other"
|
onChange={handleOtherNameChange}
|
||||||
defaultValue={modalDataState.platform}
|
/>
|
||||||
onChange={handleOtherNameChange}
|
<br />
|
||||||
/>
|
</>
|
||||||
<br/>
|
) : null}
|
||||||
</>
|
<br />
|
||||||
) : null
|
|
||||||
}
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
URL
|
URL
|
||||||
<Input
|
<Input
|
||||||
placeholder="Url to page"
|
placeholder="Url to page"
|
||||||
@@ -280,17 +282,18 @@ export default function EditSocialLinks() {
|
|||||||
value={modalDataState.url}
|
value={modalDataState.url}
|
||||||
onChange={handleUrlChange}
|
onChange={handleUrlChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{statusMessage}
|
{statusMessage}
|
||||||
</Modal>
|
</Modal>
|
||||||
<br />
|
<br />
|
||||||
<Button type="primary" onClick={() => {
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
resetModal();
|
resetModal();
|
||||||
setDisplayModal(true);
|
setDisplayModal(true);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
Add a new social link
|
Add a new social link
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ import { ServerStatusContext } from '../../../utils/server-status-context';
|
|||||||
import { FIELD_PROPS_TAGS, RESET_TIMEOUT, postConfigUpdateToAPI } from './constants';
|
import { FIELD_PROPS_TAGS, RESET_TIMEOUT, postConfigUpdateToAPI } from './constants';
|
||||||
import TextField from './form-textfield';
|
import TextField from './form-textfield';
|
||||||
import { UpdateArgs } from '../../../types/config-section';
|
import { UpdateArgs } from '../../../types/config-section';
|
||||||
import { createInputStatus, StatusState, STATUS_ERROR, STATUS_PROCESSING, STATUS_SUCCESS, STATUS_WARNING } from '../../../utils/input-statuses';
|
import {
|
||||||
|
createInputStatus,
|
||||||
|
StatusState,
|
||||||
|
STATUS_ERROR,
|
||||||
|
STATUS_PROCESSING,
|
||||||
|
STATUS_SUCCESS,
|
||||||
|
STATUS_WARNING,
|
||||||
|
} from '../../../utils/input-statuses';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
@@ -21,19 +28,14 @@ export default function EditInstanceTags() {
|
|||||||
const { instanceDetails } = serverConfig;
|
const { instanceDetails } = serverConfig;
|
||||||
const { tags = [] } = instanceDetails;
|
const { tags = [] } = instanceDetails;
|
||||||
|
|
||||||
const {
|
const { apiPath, maxLength, placeholder, configPath } = FIELD_PROPS_TAGS;
|
||||||
apiPath,
|
|
||||||
maxLength,
|
|
||||||
placeholder,
|
|
||||||
configPath,
|
|
||||||
} = FIELD_PROPS_TAGS;
|
|
||||||
|
|
||||||
let resetTimer = null;
|
let resetTimer = null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(resetTimer);
|
clearTimeout(resetTimer);
|
||||||
}
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const resetStates = () => {
|
const resetStates = () => {
|
||||||
@@ -42,7 +44,7 @@ export default function EditInstanceTags() {
|
|||||||
setFieldStatus(null);
|
setFieldStatus(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) => {
|
||||||
@@ -89,7 +91,7 @@ export default function EditInstanceTags() {
|
|||||||
|
|
||||||
// setSubmitStatusMessage('Please enter a tag');
|
// setSubmitStatusMessage('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!'));
|
setFieldStatus(createInputStatus(STATUS_WARNING, 'This tag is already used!'));
|
||||||
|
|
||||||
@@ -106,7 +108,7 @@ export default function EditInstanceTags() {
|
|||||||
const updatedTags = [...tags];
|
const updatedTags = [...tags];
|
||||||
updatedTags.splice(index, 1);
|
updatedTags.splice(index, 1);
|
||||||
postUpdateToAPI(updatedTags);
|
postUpdateToAPI(updatedTags);
|
||||||
}
|
};
|
||||||
|
|
||||||
// const {
|
// const {
|
||||||
// icon: newStatusIcon = null,
|
// icon: newStatusIcon = null,
|
||||||
@@ -115,7 +117,6 @@ export default function EditInstanceTags() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tag-editor-container">
|
<div className="tag-editor-container">
|
||||||
|
|
||||||
<Title level={3}>Add Tags</Title>
|
<Title level={3}>Add Tags</Title>
|
||||||
<p>This is a great way to categorize your Owncast server on the Directory!</p>
|
<p>This is a great way to categorize your Owncast server on the Directory!</p>
|
||||||
|
|
||||||
@@ -125,7 +126,9 @@ export default function EditInstanceTags() {
|
|||||||
handleDeleteTag(index);
|
handleDeleteTag(index);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Tag closable onClose={handleClose} key={`tag-${tag}-${index}`}>{tag}</Tag>
|
<Tag closable onClose={handleClose} key={`tag-${tag}-${index}`}>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ import { RESET_TIMEOUT, postConfigUpdateToAPI } from './constants';
|
|||||||
|
|
||||||
import { ServerStatusContext } from '../../../utils/server-status-context';
|
import { ServerStatusContext } from '../../../utils/server-status-context';
|
||||||
import TextField, { TextFieldProps } from './form-textfield';
|
import TextField, { TextFieldProps } from './form-textfield';
|
||||||
import { createInputStatus, StatusState, STATUS_ERROR, STATUS_PROCESSING, STATUS_SUCCESS } from '../../../utils/input-statuses';
|
import {
|
||||||
|
createInputStatus,
|
||||||
|
StatusState,
|
||||||
|
STATUS_ERROR,
|
||||||
|
STATUS_PROCESSING,
|
||||||
|
STATUS_SUCCESS,
|
||||||
|
} from '../../../utils/input-statuses';
|
||||||
import { UpdateArgs } from '../../../types/config-section';
|
import { UpdateArgs } from '../../../types/config-section';
|
||||||
|
|
||||||
export const TEXTFIELD_TYPE_TEXT = 'default';
|
export const TEXTFIELD_TYPE_TEXT = 'default';
|
||||||
@@ -114,11 +120,11 @@ export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
|
|||||||
onSubmit();
|
onSubmit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="textfield-with-submit-container">
|
<div className="textfield-with-submit-container">
|
||||||
<TextField
|
<TextField
|
||||||
{...textFieldProps}
|
{...textFieldProps}
|
||||||
status={status || fieldStatus}
|
status={status || fieldStatus}
|
||||||
onSubmit={null}
|
onSubmit={null}
|
||||||
@@ -126,9 +132,13 @@ export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ hasChanged ? <Button type="primary" size="small" className="submit-button" onClick={handleSubmit}>Update</Button> : null }
|
{hasChanged ? (
|
||||||
|
<Button type="primary" size="small" className="submit-button" onClick={handleSubmit}>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TextFieldWithSubmit.defaultProps = {
|
TextFieldWithSubmit.defaultProps = {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const TEXTFIELD_TYPE_URL = 'url';
|
|||||||
|
|
||||||
export interface TextFieldProps {
|
export interface TextFieldProps {
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
|
||||||
onSubmit?: () => void;
|
onSubmit?: () => void;
|
||||||
onPressEnter?: () => void;
|
onPressEnter?: () => void;
|
||||||
|
|
||||||
@@ -30,7 +30,6 @@ export interface TextFieldProps {
|
|||||||
onChange?: FieldUpdaterFunc;
|
onChange?: FieldUpdaterFunc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function TextField(props: TextFieldProps) {
|
export default function TextField(props: TextFieldProps) {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
@@ -70,11 +69,14 @@ export default function TextField(props: TextFieldProps) {
|
|||||||
if (onPressEnter) {
|
if (onPressEnter) {
|
||||||
onPressEnter();
|
onPressEnter();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
// display the appropriate Ant text field
|
// display the appropriate Ant text field
|
||||||
let Field = Input as typeof Input | typeof InputNumber | typeof Input.TextArea | typeof Input.Password;
|
let Field = Input as
|
||||||
|
| typeof Input
|
||||||
|
| typeof InputNumber
|
||||||
|
| typeof Input.TextArea
|
||||||
|
| typeof Input.Password;
|
||||||
let fieldProps = {};
|
let fieldProps = {};
|
||||||
if (type === TEXTFIELD_TYPE_TEXTAREA) {
|
if (type === TEXTFIELD_TYPE_TEXTAREA) {
|
||||||
Field = Input.TextArea;
|
Field = Input.TextArea;
|
||||||
@@ -91,12 +93,11 @@ export default function TextField(props: TextFieldProps) {
|
|||||||
fieldProps = {
|
fieldProps = {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
min: 1,
|
min: 1,
|
||||||
max: (10**maxLength) - 1,
|
max: 10 ** maxLength - 1,
|
||||||
onKeyDown: (e: React.KeyboardEvent) => {
|
onKeyDown: (e: React.KeyboardEvent) => {
|
||||||
if (e.target.value.length > maxLength - 1 )
|
if (e.target.value.length > maxLength - 1) e.preventDefault();
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
} else if (type === TEXTFIELD_TYPE_URL) {
|
} else if (type === TEXTFIELD_TYPE_URL) {
|
||||||
fieldProps = {
|
fieldProps = {
|
||||||
@@ -110,8 +111,10 @@ export default function TextField(props: TextFieldProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`textfield-container type-${type}`}>
|
<div className={`textfield-container type-${type}`}>
|
||||||
{ required ? <span className="required-label">*</span> : null }
|
{required ? <span className="required-label">*</span> : null}
|
||||||
<label htmlFor={fieldId} className="textfield-label">{label}</label>
|
<label htmlFor={fieldId} className="textfield-label">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
<div className="textfield">
|
<div className="textfield">
|
||||||
<Field
|
<Field
|
||||||
id={fieldId}
|
id={fieldId}
|
||||||
@@ -128,10 +131,10 @@ export default function TextField(props: TextFieldProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<InfoTip tip={tip} />
|
<InfoTip tip={tip} />
|
||||||
{ status ? statusMessage : null }
|
{status ? statusMessage : null}
|
||||||
{ status ? statusIcon : null }
|
{status ? statusIcon : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField.defaultProps = {
|
TextField.defaultProps = {
|
||||||
|
|||||||
@@ -26,22 +26,14 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
|
|||||||
|
|
||||||
const serverStatusData = useContext(ServerStatusContext);
|
const serverStatusData = useContext(ServerStatusContext);
|
||||||
const { setFieldInConfigState } = serverStatusData || {};
|
const { setFieldInConfigState } = serverStatusData || {};
|
||||||
|
|
||||||
const {
|
const { apiPath, checked, configPath = '', disabled = false, fieldName, label, tip } = props;
|
||||||
apiPath,
|
|
||||||
checked,
|
|
||||||
configPath = '',
|
|
||||||
disabled = false,
|
|
||||||
fieldName,
|
|
||||||
label,
|
|
||||||
tip,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const resetStates = () => {
|
const resetStates = () => {
|
||||||
setSubmitStatus('');
|
setSubmitStatus('');
|
||||||
clearTimeout(resetTimer);
|
clearTimeout(resetTimer);
|
||||||
resetTimer = null;
|
resetTimer = null;
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleChange = async (isChecked: boolean) => {
|
const handleChange = async (isChecked: boolean) => {
|
||||||
setSubmitStatus('validating');
|
setSubmitStatus('validating');
|
||||||
@@ -58,12 +50,10 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||||
}
|
};
|
||||||
|
|
||||||
const {
|
const { icon: newStatusIcon = null, message: newStatusMessage = '' } =
|
||||||
icon: newStatusIcon = null,
|
SUCCESS_STATES[submitStatus] || {};
|
||||||
message: newStatusMessage = '',
|
|
||||||
} = SUCCESS_STATES[submitStatus] || {};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="toggleswitch-container">
|
<div className="toggleswitch-container">
|
||||||
@@ -74,18 +64,20 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
defaultChecked={checked}
|
defaultChecked={checked}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
checkedChildren="ON"
|
checkedChildren="ON"
|
||||||
unCheckedChildren="OFF"
|
unCheckedChildren="OFF"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<span className="label">{label} <InfoTip tip={tip} /></span>
|
<span className="label">
|
||||||
|
{label} <InfoTip tip={tip} />
|
||||||
|
</span>
|
||||||
{submitStatus}
|
{submitStatus}
|
||||||
</div>
|
</div>
|
||||||
<div className={`status-message ${submitStatus || ''}`}>
|
<div className={`status-message ${submitStatus || ''}`}>
|
||||||
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
|
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ToggleSwitch.defaultProps = {
|
ToggleSwitch.defaultProps = {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Select } from "antd";
|
import { Select } from 'antd';
|
||||||
import { SocialHandleDropdownItem } from "../../../types/config-section";
|
import { SocialHandleDropdownItem } from '../../../types/config-section';
|
||||||
import { NEXT_PUBLIC_API_HOST } from '../../../utils/apis';
|
import { NEXT_PUBLIC_API_HOST } from '../../../utils/apis';
|
||||||
import { OTHER_SOCIAL_HANDLE_OPTION } from './constants';
|
import { OTHER_SOCIAL_HANDLE_OPTION } from './constants';
|
||||||
|
|
||||||
|
|
||||||
interface DropdownProps {
|
interface DropdownProps {
|
||||||
iconList: SocialHandleDropdownItem[];
|
iconList: SocialHandleDropdownItem[];
|
||||||
selectedOption: string;
|
selectedOption: string;
|
||||||
@@ -12,7 +11,6 @@ interface DropdownProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SocialDropdown({ iconList, selectedOption, onSelected }: DropdownProps) {
|
export default function SocialDropdown({ iconList, selectedOption, onSelected }: DropdownProps) {
|
||||||
|
|
||||||
const handleSelected = value => {
|
const handleSelected = value => {
|
||||||
if (onSelected) {
|
if (onSelected) {
|
||||||
onSelected(value);
|
onSelected(value);
|
||||||
@@ -21,9 +19,16 @@ export default function SocialDropdown({ iconList, selectedOption, onSelected }:
|
|||||||
const inititalSelected = selectedOption === '' ? null : selectedOption;
|
const inititalSelected = selectedOption === '' ? null : selectedOption;
|
||||||
return (
|
return (
|
||||||
<div className="social-dropdown-container">
|
<div className="social-dropdown-container">
|
||||||
<p className="">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="">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.</p>
|
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="">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
style={{ width: 240 }}
|
style={{ width: 240 }}
|
||||||
className="social-dropdown"
|
className="social-dropdown"
|
||||||
@@ -33,7 +38,7 @@ export default function SocialDropdown({ iconList, selectedOption, onSelected }:
|
|||||||
onSelect={handleSelected}
|
onSelect={handleSelected}
|
||||||
>
|
>
|
||||||
{iconList.map(item => {
|
{iconList.map(item => {
|
||||||
const { platform, icon, key } = item;
|
const { platform, icon, key } = item;
|
||||||
return (
|
return (
|
||||||
<Select.Option className="social-option" key={`platform-${key}`} value={key}>
|
<Select.Option className="social-option" key={`platform-${key}`} value={key}>
|
||||||
<span className="option-icon">
|
<span className="option-icon">
|
||||||
@@ -42,9 +47,12 @@ export default function SocialDropdown({ iconList, selectedOption, onSelected }:
|
|||||||
<span className="option-label">{platform}</span>
|
<span className="option-label">{platform}</span>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
);
|
);
|
||||||
})
|
})}
|
||||||
}
|
<Select.Option
|
||||||
<Select.Option className="social-option" key={`platform-${OTHER_SOCIAL_HANDLE_OPTION}`} value={OTHER_SOCIAL_HANDLE_OPTION}>
|
className="social-option"
|
||||||
|
key={`platform-${OTHER_SOCIAL_HANDLE_OPTION}`}
|
||||||
|
value={OTHER_SOCIAL_HANDLE_OPTION}
|
||||||
|
>
|
||||||
Other...
|
Other...
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import React, { useContext, useState, useEffect } from 'react';
|
import React, { useContext, useState, useEffect } from 'react';
|
||||||
import { Typography, Slider, } from 'antd';
|
import { Typography, Slider } from 'antd';
|
||||||
import { ServerStatusContext } from '../../../utils/server-status-context';
|
import { ServerStatusContext } from '../../../utils/server-status-context';
|
||||||
import { API_VIDEO_SEGMENTS, SUCCESS_STATES, RESET_TIMEOUT,postConfigUpdateToAPI } from './constants';
|
import {
|
||||||
|
API_VIDEO_SEGMENTS,
|
||||||
|
SUCCESS_STATES,
|
||||||
|
RESET_TIMEOUT,
|
||||||
|
postConfigUpdateToAPI,
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
@@ -28,9 +33,7 @@ interface SegmentToolTipProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SegmentToolTip({ value }: SegmentToolTipProps) {
|
function SegmentToolTip({ value }: SegmentToolTipProps) {
|
||||||
return (
|
return <span className="segment-tip">{value}</span>;
|
||||||
<span className="segment-tip">{value}</span>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function VideoLatency() {
|
export default function VideoLatency() {
|
||||||
@@ -57,8 +60,8 @@ export default function VideoLatency() {
|
|||||||
setSubmitStatusMessage('');
|
setSubmitStatusMessage('');
|
||||||
resetTimer = null;
|
resetTimer = null;
|
||||||
clearTimeout(resetTimer);
|
clearTimeout(resetTimer);
|
||||||
}
|
};
|
||||||
|
|
||||||
// posts all the variants at once as an array obj
|
// posts all the variants at once as an array obj
|
||||||
const postUpdateToAPI = async (postValue: any) => {
|
const postUpdateToAPI = async (postValue: any) => {
|
||||||
await postConfigUpdateToAPI({
|
await postConfigUpdateToAPI({
|
||||||
@@ -66,9 +69,9 @@ export default function VideoLatency() {
|
|||||||
data: { value: postValue },
|
data: { value: postValue },
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setFieldInConfigState({
|
setFieldInConfigState({
|
||||||
fieldName: 'latencyLevel',
|
fieldName: 'latencyLevel',
|
||||||
value: postValue,
|
value: postValue,
|
||||||
path: 'videoSettings'
|
path: 'videoSettings',
|
||||||
});
|
});
|
||||||
|
|
||||||
setSubmitStatus('success');
|
setSubmitStatus('success');
|
||||||
@@ -83,17 +86,15 @@ export default function VideoLatency() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { icon: newStatusIcon = null, message: newStatusMessage = '' } =
|
||||||
icon: newStatusIcon = null,
|
SUCCESS_STATES[submitStatus] || {};
|
||||||
message: newStatusMessage = '',
|
|
||||||
} = SUCCESS_STATES[submitStatus] || {};
|
|
||||||
|
|
||||||
const statusMessage = (
|
const statusMessage = (
|
||||||
<div className={`status-message ${submitStatus || ''}`}>
|
<div className={`status-message ${submitStatus || ''}`}>
|
||||||
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
|
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = value => {
|
const handleChange = value => {
|
||||||
postUpdateToAPI(value);
|
postUpdateToAPI(value);
|
||||||
};
|
};
|
||||||
@@ -102,11 +103,13 @@ export default function VideoLatency() {
|
|||||||
<div className="module-container config-video-segements-conatiner">
|
<div className="module-container config-video-segements-conatiner">
|
||||||
<Title level={3}>Latency Buffer</Title>
|
<Title level={3}>Latency Buffer</Title>
|
||||||
<p>
|
<p>
|
||||||
There are trade-offs when cosidering video latency and reliability. Blah blah .. better wording here needed.
|
There are trade-offs when cosidering video latency and reliability. Blah blah .. better
|
||||||
|
wording here needed.
|
||||||
</p>
|
</p>
|
||||||
<br /><br />
|
<br />
|
||||||
|
<br />
|
||||||
<div className="segment-slider">
|
<div className="segment-slider">
|
||||||
<Slider
|
<Slider
|
||||||
tooltipVisible
|
tooltipVisible
|
||||||
tipFormatter={value => <SegmentToolTip value={SLIDER_COMMENTS[value]} />}
|
tipFormatter={value => <SegmentToolTip value={SLIDER_COMMENTS[value]} />}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@@ -120,4 +123,4 @@ export default function VideoLatency() {
|
|||||||
{statusMessage}
|
{statusMessage}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ const VIDEO_VARIANT_DEFAULTS = {
|
|||||||
defaultValue: 800,
|
defaultValue: 800,
|
||||||
unit: 'kbps',
|
unit: 'kbps',
|
||||||
incrementBy: 100,
|
incrementBy: 100,
|
||||||
tip: 'nothing to see here'
|
tip: 'nothing to see here',
|
||||||
},
|
},
|
||||||
videoPassthrough: {
|
videoPassthrough: {
|
||||||
tip: 'If No is selected, then you should set your desired Video Bitrate.'
|
tip: 'If No is selected, then you should set your desired Video Bitrate.',
|
||||||
},
|
},
|
||||||
audioPassthrough: {
|
audioPassthrough: {
|
||||||
tip: 'If No is selected, then you should set your desired Audio Bitrate.'
|
tip: 'If No is selected, then you should set your desired Audio Bitrate.',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,8 +47,10 @@ interface VideoVariantFormProps {
|
|||||||
onUpdateField: FieldUpdaterFunc;
|
onUpdateField: FieldUpdaterFunc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, onUpdateField }: VideoVariantFormProps) {
|
export default function VideoVariantForm({
|
||||||
|
dataState = DEFAULT_VARIANT_STATE,
|
||||||
|
onUpdateField,
|
||||||
|
}: VideoVariantFormProps) {
|
||||||
const handleFramerateChange = (value: number) => {
|
const handleFramerateChange = (value: number) => {
|
||||||
onUpdateField({ fieldName: 'framerate', value });
|
onUpdateField({ fieldName: 'framerate', value });
|
||||||
};
|
};
|
||||||
@@ -65,8 +67,8 @@ export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, on
|
|||||||
onUpdateField({ fieldName: 'videoPassthrough', value });
|
onUpdateField({ fieldName: 'videoPassthrough', value });
|
||||||
};
|
};
|
||||||
const handleVideoCpuUsageLevelChange = (value: number) => {
|
const handleVideoCpuUsageLevelChange = (value: number) => {
|
||||||
onUpdateField({ fieldName: 'cpuUsageLevel', value })
|
onUpdateField({ fieldName: 'cpuUsageLevel', value });
|
||||||
}
|
};
|
||||||
|
|
||||||
const framerateDefaults = VIDEO_VARIANT_DEFAULTS.framerate;
|
const framerateDefaults = VIDEO_VARIANT_DEFAULTS.framerate;
|
||||||
const framerateMin = framerateDefaults.min;
|
const framerateMin = framerateDefaults.min;
|
||||||
@@ -91,24 +93,27 @@ export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, on
|
|||||||
return (
|
return (
|
||||||
<div className="variant-form">
|
<div className="variant-form">
|
||||||
<div className="section-intro">
|
<div className="section-intro">
|
||||||
Say a thing here about how this all works.
|
Say a thing here about how this all works. Read more{' '}
|
||||||
|
<a href="https://owncast.online/docs/configuration/">here</a>.
|
||||||
Read more <a href="https://owncast.online/docs/configuration/">here</a>.
|
<br />
|
||||||
<br /><br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ENCODER PRESET FIELD */}
|
{/* ENCODER PRESET FIELD */}
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<div className="form-component">
|
<div className="form-component">
|
||||||
<CPUUsageSelector defaultValue={dataState.cpuUsageLevel} onChange={handleVideoCpuUsageLevelChange} />
|
<CPUUsageSelector
|
||||||
{selectedPresetNote ? <span className="selected-value-note">{selectedPresetNote}</span> : null }
|
defaultValue={dataState.cpuUsageLevel}
|
||||||
|
onChange={handleVideoCpuUsageLevelChange}
|
||||||
|
/>
|
||||||
|
{selectedPresetNote ? (
|
||||||
|
<span className="selected-value-note">{selectedPresetNote}</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* VIDEO PASSTHROUGH FIELD */}
|
{/* VIDEO PASSTHROUGH FIELD */}
|
||||||
<div style={{ display: 'none'}}>
|
<div style={{ display: 'none' }}>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<p className="label">
|
<p className="label">
|
||||||
<InfoTip tip={VIDEO_VARIANT_DEFAULTS.videoPassthrough.tip} />
|
<InfoTip tip={VIDEO_VARIANT_DEFAULTS.videoPassthrough.tip} />
|
||||||
@@ -147,19 +152,20 @@ export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, on
|
|||||||
[videoBRMax]: `${videoBRMax} ${videoBRUnit}`,
|
[videoBRMax]: `${videoBRMax} ${videoBRUnit}`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{selectedVideoBRnote ? <span className="selected-value-note">{selectedVideoBRnote}</span> : null }
|
{selectedVideoBRnote ? (
|
||||||
|
<span className="selected-value-note">{selectedVideoBRnote}</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
|
||||||
<br /><br /><br /><br />
|
|
||||||
|
|
||||||
<Collapse>
|
<Collapse>
|
||||||
<Panel header="Advanced Settings" key="1">
|
<Panel header="Advanced Settings" key="1">
|
||||||
<div className="section-intro">
|
<div className="section-intro">Touch if you dare.</div>
|
||||||
Touch if you dare.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* FRAME RATE FIELD */}
|
{/* FRAME RATE FIELD */}
|
||||||
<div className="field">
|
<div className="field">
|
||||||
@@ -182,8 +188,9 @@ export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, on
|
|||||||
[framerateMax]: `${framerateMax} ${framerateUnit}`,
|
[framerateMax]: `${framerateMax} ${framerateUnit}`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{selectedFramerateNote ? <span className="selected-value-note">{selectedFramerateNote}</span> : null }
|
{selectedFramerateNote ? (
|
||||||
|
<span className="selected-value-note">{selectedFramerateNote}</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -203,7 +210,7 @@ export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, on
|
|||||||
checkedChildren="Yes"
|
checkedChildren="Yes"
|
||||||
unCheckedChildren="No"
|
unCheckedChildren="No"
|
||||||
/>
|
/>
|
||||||
{dataState.audioPassthrough ? <span className="note">Same as source</span>: null}
|
{dataState.audioPassthrough ? <span className="note">Same as source</span> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -230,13 +237,13 @@ export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, on
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{selectedAudioBRnote ? <span className="selected-value-note">{selectedAudioBRnote}</span> : null }
|
{selectedAudioBRnote ? (
|
||||||
|
<span className="selected-value-note">{selectedAudioBRnote}</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ import { DeleteOutlined } from '@ant-design/icons';
|
|||||||
import { ServerStatusContext } from '../../../utils/server-status-context';
|
import { ServerStatusContext } from '../../../utils/server-status-context';
|
||||||
import { UpdateArgs, VideoVariant } from '../../../types/config-section';
|
import { UpdateArgs, VideoVariant } from '../../../types/config-section';
|
||||||
import VideoVariantForm from './video-variant-form';
|
import VideoVariantForm from './video-variant-form';
|
||||||
import { API_VIDEO_VARIANTS, DEFAULT_VARIANT_STATE, SUCCESS_STATES, RESET_TIMEOUT, postConfigUpdateToAPI } from './constants';
|
import {
|
||||||
|
API_VIDEO_VARIANTS,
|
||||||
|
DEFAULT_VARIANT_STATE,
|
||||||
|
SUCCESS_STATES,
|
||||||
|
RESET_TIMEOUT,
|
||||||
|
postConfigUpdateToAPI,
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
@@ -19,7 +25,7 @@ export default function CurrentVariantsTable() {
|
|||||||
|
|
||||||
// current data inside modal
|
// current data inside modal
|
||||||
const [modalDataState, setModalDataState] = useState(DEFAULT_VARIANT_STATE);
|
const [modalDataState, setModalDataState] = useState(DEFAULT_VARIANT_STATE);
|
||||||
|
|
||||||
const [submitStatus, setSubmitStatus] = useState(null);
|
const [submitStatus, setSubmitStatus] = useState(null);
|
||||||
const [submitStatusMessage, setSubmitStatusMessage] = useState('');
|
const [submitStatusMessage, setSubmitStatusMessage] = useState('');
|
||||||
|
|
||||||
@@ -39,13 +45,13 @@ export default function CurrentVariantsTable() {
|
|||||||
setSubmitStatusMessage('');
|
setSubmitStatusMessage('');
|
||||||
resetTimer = null;
|
resetTimer = null;
|
||||||
clearTimeout(resetTimer);
|
clearTimeout(resetTimer);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleModalCancel = () => {
|
const handleModalCancel = () => {
|
||||||
setDisplayModal(false);
|
setDisplayModal(false);
|
||||||
setEditId(-1);
|
setEditId(-1);
|
||||||
setModalDataState(DEFAULT_VARIANT_STATE);
|
setModalDataState(DEFAULT_VARIANT_STATE);
|
||||||
}
|
};
|
||||||
|
|
||||||
// posts all the variants at once as an array obj
|
// posts all the variants at once as an array obj
|
||||||
const postUpdateToAPI = async (postValue: any) => {
|
const postUpdateToAPI = async (postValue: any) => {
|
||||||
@@ -53,7 +59,11 @@ export default function CurrentVariantsTable() {
|
|||||||
apiPath: API_VIDEO_VARIANTS,
|
apiPath: API_VIDEO_VARIANTS,
|
||||||
data: { value: postValue },
|
data: { value: postValue },
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setFieldInConfigState({ fieldName: 'videoQualityVariants', value: postValue, path: 'videoSettings' });
|
setFieldInConfigState({
|
||||||
|
fieldName: 'videoQualityVariants',
|
||||||
|
value: postValue,
|
||||||
|
path: 'videoSettings',
|
||||||
|
});
|
||||||
|
|
||||||
// close modal
|
// close modal
|
||||||
setModalProcessing(false);
|
setModalProcessing(false);
|
||||||
@@ -76,10 +86,8 @@ export default function CurrentVariantsTable() {
|
|||||||
// close modal when api is done
|
// close modal when api is done
|
||||||
const handleModalOk = () => {
|
const handleModalOk = () => {
|
||||||
setModalProcessing(true);
|
setModalProcessing(true);
|
||||||
|
|
||||||
const postData = [
|
const postData = [...videoQualityVariants];
|
||||||
...videoQualityVariants,
|
|
||||||
];
|
|
||||||
if (editId === -1) {
|
if (editId === -1) {
|
||||||
postData.push(modalDataState);
|
postData.push(modalDataState);
|
||||||
} else {
|
} else {
|
||||||
@@ -89,11 +97,9 @@ export default function CurrentVariantsTable() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteVariant = index => {
|
const handleDeleteVariant = index => {
|
||||||
const postData = [
|
const postData = [...videoQualityVariants];
|
||||||
...videoQualityVariants,
|
|
||||||
];
|
|
||||||
postData.splice(index, 1);
|
postData.splice(index, 1);
|
||||||
postUpdateToAPI(postData)
|
postUpdateToAPI(postData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateField = ({ fieldName, value }: UpdateArgs) => {
|
const handleUpdateField = ({ fieldName, value }: UpdateArgs) => {
|
||||||
@@ -101,41 +107,37 @@ export default function CurrentVariantsTable() {
|
|||||||
...modalDataState,
|
...modalDataState,
|
||||||
[fieldName]: value,
|
[fieldName]: value,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const { icon: newStatusIcon = null, message: newStatusMessage = '' } =
|
||||||
|
SUCCESS_STATES[submitStatus] || {};
|
||||||
|
|
||||||
const {
|
|
||||||
icon: newStatusIcon = null,
|
|
||||||
message: newStatusMessage = '',
|
|
||||||
} = SUCCESS_STATES[submitStatus] || {};
|
|
||||||
|
|
||||||
const cpuUsageLevelLabelMap = {
|
const cpuUsageLevelLabelMap = {
|
||||||
1: 'lowest',
|
1: 'lowest',
|
||||||
2: 'low',
|
2: 'low',
|
||||||
3: 'medium',
|
3: 'medium',
|
||||||
4: 'high',
|
4: 'high',
|
||||||
5: 'highest'
|
5: 'highest',
|
||||||
};
|
};
|
||||||
|
|
||||||
const videoQualityColumns: ColumnsType<VideoVariant> = [
|
const videoQualityColumns: ColumnsType<VideoVariant> = [
|
||||||
{
|
{
|
||||||
title: "#",
|
title: '#',
|
||||||
dataIndex: "key",
|
dataIndex: 'key',
|
||||||
key: "key"
|
key: 'key',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Video bitrate",
|
title: 'Video bitrate',
|
||||||
dataIndex: "videoBitrate",
|
dataIndex: 'videoBitrate',
|
||||||
key: "videoBitrate",
|
key: 'videoBitrate',
|
||||||
render: (bitrate: number) =>
|
render: (bitrate: number) => (!bitrate ? 'Same as source' : `${bitrate} kbps`),
|
||||||
!bitrate ? "Same as source" : `${bitrate} kbps`,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "CPU Usage",
|
title: 'CPU Usage',
|
||||||
dataIndex: "cpuUsageLevel",
|
dataIndex: 'cpuUsageLevel',
|
||||||
key: "cpuUsageLevel",
|
key: 'cpuUsageLevel',
|
||||||
render: (level: string) =>
|
render: (level: string) => (!level ? 'n/a' : cpuUsageLevelLabelMap[level]),
|
||||||
!level ? "n/a" : cpuUsageLevelLabelMap[level],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
@@ -145,11 +147,15 @@ export default function CurrentVariantsTable() {
|
|||||||
const index = data.key - 1;
|
const index = data.key - 1;
|
||||||
return (
|
return (
|
||||||
<span className="actions">
|
<span className="actions">
|
||||||
<Button type="primary" size="small" onClick={() => {
|
<Button
|
||||||
setEditId(index);
|
type="primary"
|
||||||
setModalDataState(videoQualityVariants[index]);
|
size="small"
|
||||||
setDisplayModal(true);
|
onClick={() => {
|
||||||
}}>
|
setEditId(index);
|
||||||
|
setModalDataState(videoQualityVariants[index]);
|
||||||
|
setDisplayModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -160,11 +166,12 @@ export default function CurrentVariantsTable() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDeleteVariant(index);
|
handleDeleteVariant(index);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)},
|
);
|
||||||
},
|
},
|
||||||
];
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const statusMessage = (
|
const statusMessage = (
|
||||||
<div className={`status-message ${submitStatus || ''}`}>
|
<div className={`status-message ${submitStatus || ''}`}>
|
||||||
@@ -172,12 +179,15 @@ export default function CurrentVariantsTable() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const videoQualityVariantData = videoQualityVariants.map((variant, index) => ({ key: index + 1, ...variant }));
|
const videoQualityVariantData = videoQualityVariants.map((variant, index) => ({
|
||||||
|
key: index + 1,
|
||||||
|
...variant,
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title level={3}>Current Variants</Title>
|
<Title level={3}>Current Variants</Title>
|
||||||
|
|
||||||
{statusMessage}
|
{statusMessage}
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
@@ -195,22 +205,21 @@ export default function CurrentVariantsTable() {
|
|||||||
onCancel={handleModalCancel}
|
onCancel={handleModalCancel}
|
||||||
confirmLoading={modalProcessing}
|
confirmLoading={modalProcessing}
|
||||||
>
|
>
|
||||||
<VideoVariantForm
|
<VideoVariantForm dataState={{ ...modalDataState }} onUpdateField={handleUpdateField} />
|
||||||
dataState={{...modalDataState}}
|
|
||||||
onUpdateField={handleUpdateField}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{statusMessage}
|
{statusMessage}
|
||||||
</Modal>
|
</Modal>
|
||||||
<br />
|
<br />
|
||||||
<Button type="primary" onClick={() => {
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
setEditId(-1);
|
setEditId(-1);
|
||||||
setModalDataState(DEFAULT_VARIANT_STATE);
|
setModalDataState(DEFAULT_VARIANT_STATE);
|
||||||
setDisplayModal(true);
|
setDisplayModal(true);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
Add a new variant
|
Add a new variant
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react';
|
import React, { useState, useEffect, useContext } from 'react';
|
||||||
import { Typography, Button } from "antd";
|
import { Typography, Button } from 'antd';
|
||||||
import { FormItemProps } from 'antd/lib/form';
|
import { FormItemProps } from 'antd/lib/form';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import MarkdownIt from 'markdown-it';
|
import MarkdownIt from 'markdown-it';
|
||||||
|
|
||||||
import { ServerStatusContext } from '../utils/server-status-context';
|
import { ServerStatusContext } from '../utils/server-status-context';
|
||||||
import { postConfigUpdateToAPI, RESET_TIMEOUT, SUCCESS_STATES, API_CUSTOM_CONTENT} from './components/config/constants';
|
import {
|
||||||
|
postConfigUpdateToAPI,
|
||||||
|
RESET_TIMEOUT,
|
||||||
|
SUCCESS_STATES,
|
||||||
|
API_CUSTOM_CONTENT,
|
||||||
|
} from './components/config/constants';
|
||||||
|
|
||||||
import 'react-markdown-editor-lite/lib/index.css';
|
import 'react-markdown-editor-lite/lib/index.css';
|
||||||
|
|
||||||
@@ -14,91 +19,98 @@ const { Title } = Typography;
|
|||||||
const mdParser = new MarkdownIt(/* Markdown-it options */);
|
const mdParser = new MarkdownIt(/* Markdown-it options */);
|
||||||
|
|
||||||
const MdEditor = dynamic(() => import('react-markdown-editor-lite'), {
|
const MdEditor = dynamic(() => import('react-markdown-editor-lite'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function PageContentEditor() {
|
export default function PageContentEditor() {
|
||||||
const [content, setContent] = useState('');
|
const [content, setContent] = useState('');
|
||||||
const [submitStatus, setSubmitStatus] = useState<FormItemProps['validateStatus']>('');
|
const [submitStatus, setSubmitStatus] = useState<FormItemProps['validateStatus']>('');
|
||||||
const [submitStatusMessage, setSubmitStatusMessage] = useState('');
|
const [submitStatusMessage, setSubmitStatusMessage] = useState('');
|
||||||
const [hasChanged, setHasChanged] = useState(false);
|
const [hasChanged, setHasChanged] = useState(false);
|
||||||
|
|
||||||
const serverStatusData = useContext(ServerStatusContext);
|
|
||||||
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
|
|
||||||
|
|
||||||
const { instanceDetails } = serverConfig;
|
|
||||||
const { extraPageContent: initialContent } = instanceDetails;
|
|
||||||
|
|
||||||
|
|
||||||
let resetTimer = null;
|
const serverStatusData = useContext(ServerStatusContext);
|
||||||
|
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
|
||||||
|
|
||||||
function handleEditorChange({ text }) {
|
const { instanceDetails } = serverConfig;
|
||||||
setContent(text);
|
const { extraPageContent: initialContent } = instanceDetails;
|
||||||
if (text !== initialContent && !hasChanged) {
|
|
||||||
setHasChanged(true);
|
|
||||||
} else if (text === initialContent && hasChanged) {
|
|
||||||
setHasChanged(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear out any validation states and messaging
|
let resetTimer = null;
|
||||||
const resetStates = () => {
|
|
||||||
setSubmitStatus('');
|
function handleEditorChange({ text }) {
|
||||||
|
setContent(text);
|
||||||
|
if (text !== initialContent && !hasChanged) {
|
||||||
|
setHasChanged(true);
|
||||||
|
} else if (text === initialContent && hasChanged) {
|
||||||
setHasChanged(false);
|
setHasChanged(false);
|
||||||
clearTimeout(resetTimer);
|
|
||||||
resetTimer = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// posts all the tags at once as an array obj
|
|
||||||
async function handleSave() {
|
|
||||||
setSubmitStatus('validating');
|
|
||||||
await postConfigUpdateToAPI({
|
|
||||||
apiPath: API_CUSTOM_CONTENT,
|
|
||||||
data: { value: content },
|
|
||||||
onSuccess: () => {
|
|
||||||
setFieldInConfigState({ fieldName: 'extraPageContent', value: content, path: 'instanceDetails' });
|
|
||||||
setSubmitStatus('success');
|
|
||||||
},
|
|
||||||
onError: (message: string) => {
|
|
||||||
setSubmitStatus('error');
|
|
||||||
setSubmitStatusMessage(`There was an error: ${message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
useEffect(() => {
|
|
||||||
setContent(initialContent);
|
|
||||||
}, [instanceDetails]);
|
|
||||||
|
|
||||||
const {
|
// Clear out any validation states and messaging
|
||||||
icon: newStatusIcon = null,
|
const resetStates = () => {
|
||||||
message: newStatusMessage = '',
|
setSubmitStatus('');
|
||||||
} = SUCCESS_STATES[submitStatus] || {};
|
setHasChanged(false);
|
||||||
|
clearTimeout(resetTimer);
|
||||||
|
resetTimer = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// posts all the tags at once as an array obj
|
||||||
|
async function handleSave() {
|
||||||
|
setSubmitStatus('validating');
|
||||||
|
await postConfigUpdateToAPI({
|
||||||
|
apiPath: API_CUSTOM_CONTENT,
|
||||||
|
data: { value: content },
|
||||||
|
onSuccess: () => {
|
||||||
|
setFieldInConfigState({
|
||||||
|
fieldName: 'extraPageContent',
|
||||||
|
value: content,
|
||||||
|
path: 'instanceDetails',
|
||||||
|
});
|
||||||
|
setSubmitStatus('success');
|
||||||
|
},
|
||||||
|
onError: (message: string) => {
|
||||||
|
setSubmitStatus('error');
|
||||||
|
setSubmitStatusMessage(`There was an error: ${message}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<div className="config-page-content-form">
|
setContent(initialContent);
|
||||||
<Title level={2}>Edit custom content</Title>
|
}, [instanceDetails]);
|
||||||
|
|
||||||
<p>Add some content about your site with the Markdown editor below. This content shows up at the bottom half of your Owncast page.</p>
|
const { icon: newStatusIcon = null, message: newStatusMessage = '' } =
|
||||||
|
SUCCESS_STATES[submitStatus] || {};
|
||||||
|
|
||||||
<MdEditor
|
return (
|
||||||
style={{ height: "30em" }}
|
<div className="config-page-content-form">
|
||||||
value={content}
|
<Title level={2}>Edit custom content</Title>
|
||||||
renderHTML={(c: string) => mdParser.render(c)}
|
|
||||||
onChange={handleEditorChange}
|
<p>
|
||||||
config={{
|
Add some content about your site with the Markdown editor below. This content shows up at
|
||||||
htmlClass: 'markdown-editor-preview-pane',
|
the bottom half of your Owncast page.
|
||||||
markdownClass: 'markdown-editor-pane',
|
</p>
|
||||||
}}
|
|
||||||
/>
|
<MdEditor
|
||||||
<div className="page-content-actions">
|
style={{ height: '30em' }}
|
||||||
{ hasChanged ? <Button type="primary" onClick={handleSave}>Save</Button> : null }
|
value={content}
|
||||||
<div className={`status-message ${submitStatus || ''}`}>
|
renderHTML={(c: string) => mdParser.render(c)}
|
||||||
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
|
onChange={handleEditorChange}
|
||||||
</div>
|
config={{
|
||||||
|
htmlClass: 'markdown-editor-preview-pane',
|
||||||
|
markdownClass: 'markdown-editor-pane',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="page-content-actions">
|
||||||
|
{hasChanged ? (
|
||||||
|
<Button type="primary" onClick={handleSave}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<div className={`status-message ${submitStatus || ''}`}>
|
||||||
|
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,12 @@ export default function PublicFacingDetails() {
|
|||||||
<div className={configStyles.publicDetailsContainer}>
|
<div className={configStyles.publicDetailsContainer}>
|
||||||
<div className={configStyles.textFieldsSection}>
|
<div className={configStyles.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>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ export default function ConfigServerDetails() {
|
|||||||
|
|
||||||
<div className="config-server-details-container">
|
<div className="config-server-details-container">
|
||||||
<EditServerDetails />
|
<EditServerDetails />
|
||||||
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ export default function ConfigSocialThings() {
|
|||||||
<div className="config-social-items">
|
<div className="config-social-items">
|
||||||
<Title level={2}>Social Items</Title>
|
<Title level={2}>Social Items</Title>
|
||||||
|
|
||||||
<EditDirectoryDetails />
|
<EditDirectoryDetails />
|
||||||
<EditSocialLinks />
|
<EditSocialLinks />
|
||||||
<EditInstanceTags />
|
<EditInstanceTags />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,17 @@ export default function ConfigVideoSettings() {
|
|||||||
return (
|
return (
|
||||||
<div className="config-video-variants">
|
<div className="config-video-variants">
|
||||||
<Title level={2}>Video configuration</Title>
|
<Title level={2}>Video configuration</Title>
|
||||||
<p>Learn more about configuring Owncast <a href="https://owncast.online/docs/configuration">by visiting the documentation.</a></p>
|
<p>
|
||||||
|
Learn more about configuring Owncast{' '}
|
||||||
<VideoLatency />
|
<a href="https://owncast.online/docs/configuration">by visiting the documentation.</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<br /><br />
|
<VideoLatency />
|
||||||
|
|
||||||
<VideoVariantsTable />
|
<br />
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<VideoVariantsTable />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,167 +1,210 @@
|
|||||||
import { Button, Card, Col, Divider, Result, Row } from 'antd'
|
import { Button, Card, Col, Divider, Result, Row } from 'antd';
|
||||||
import Meta from 'antd/lib/card/Meta'
|
import Meta from 'antd/lib/card/Meta';
|
||||||
import Title from 'antd/lib/typography/Title'
|
import Title from 'antd/lib/typography/Title';
|
||||||
import {
|
import {
|
||||||
AlertOutlined,
|
AlertOutlined,
|
||||||
ApiTwoTone,
|
ApiTwoTone,
|
||||||
BookOutlined,
|
BookOutlined,
|
||||||
BugTwoTone,
|
BugTwoTone,
|
||||||
CameraTwoTone,
|
CameraTwoTone,
|
||||||
DatabaseTwoTone,
|
DatabaseTwoTone,
|
||||||
EditTwoTone,
|
EditTwoTone,
|
||||||
Html5TwoTone,
|
Html5TwoTone,
|
||||||
LinkOutlined,
|
LinkOutlined,
|
||||||
QuestionCircleTwoTone,
|
QuestionCircleTwoTone,
|
||||||
SettingTwoTone,
|
SettingTwoTone,
|
||||||
SlidersTwoTone,
|
SlidersTwoTone,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons';
|
||||||
import React from 'react'
|
import React from 'react';
|
||||||
|
|
||||||
interface Props { }
|
interface Props {}
|
||||||
|
|
||||||
export default function Help(props: Props) {
|
export default function Help(props: Props) {
|
||||||
const questions = [
|
const questions = [
|
||||||
{
|
{
|
||||||
icon: <SettingTwoTone style={{ fontSize: '24px' }} />,
|
icon: <SettingTwoTone style={{ fontSize: '24px' }} />,
|
||||||
title: "I want to configure my owncast instance",
|
title: 'I want to configure my owncast instance',
|
||||||
content: (
|
content: (
|
||||||
<div>
|
|
||||||
<a href="https://owncast.online/docs/configuration/" target="_blank" rel="noopener noreferrer"><LinkOutlined/> Learn more</a>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <CameraTwoTone style={{ fontSize: '24px' }} />,
|
|
||||||
title: "I need help configuring my broadcasting software",
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
<a href="https://owncast.online/docs/broadcasting/" target="_blank" rel="noopener noreferrer"><LinkOutlined/> Learn more</a>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Html5TwoTone style={{ fontSize: '24px' }} />,
|
|
||||||
title: "I want to embed my stream into another site",
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
<a href="https://owncast.online/docs/embed/" target="_blank" rel="noopener noreferrer"><LinkOutlined/> Learn more</a>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <EditTwoTone style={{ fontSize: '24px' }} />,
|
|
||||||
title: "I want to customize my website",
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
<a href="https://owncast.online/docs/website/" target="_blank" rel="noopener noreferrer"><LinkOutlined/> Learn more</a>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <SlidersTwoTone style={{ fontSize: '24px' }} />,
|
|
||||||
title: "I want to tweak my encoding quality or performance",
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
<a href="https://owncast.online/docs/encoding/" target="_blank" rel="noopener noreferrer"><LinkOutlined/> Learn more</a>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <DatabaseTwoTone style={{ fontSize: '24px' }} />,
|
|
||||||
title: "I want to offload my video to an external storage provider",
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
<a href="https://owncast.online/docs/s3/" target="_blank" rel="noopener noreferrer"><LinkOutlined/> Learn more</a>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const otherResources = [
|
|
||||||
{
|
|
||||||
icon: <BugTwoTone style={{ fontSize: '24px' }} />,
|
|
||||||
title: "I found a bug",
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
If you found a bug, then please
|
|
||||||
<a href="https://github.com/owncast/owncast/issues/new/choose" target="_blank" rel="noopener noreferrer"> let us know</a>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <QuestionCircleTwoTone style={{ fontSize: '24px' }} />,
|
|
||||||
title: "I have a general question",
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
Most general questions are answered in our
|
|
||||||
<a href="https://owncast.online/docs/faq/" target="_blank" rel="noopener noreferrer"> FAQ</a> or exist in our <a href="https://github.com/owncast/owncast/discussions">discussions</a>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <ApiTwoTone style={{ fontSize: '24px' }} />,
|
|
||||||
title: "I want to use the API",
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
You can view the API documentation for either the
|
|
||||||
<a href="https://owncast.online/api/latest" target="_blank" rel="noopener noreferrer"> latest </a>
|
|
||||||
or
|
|
||||||
<a href="https://owncast.online/api/development" target="_blank" rel="noopener noreferrer"> development </a>
|
|
||||||
release.
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
<div>
|
||||||
<Title style={{textAlign: 'center'}}>How can we help you?</Title>
|
<a
|
||||||
<Row gutter={[16, 16]} justify="space-around" align="middle">
|
href="https://owncast.online/docs/configuration/"
|
||||||
<Col xs={24} lg={12} style={{textAlign: 'center'}}>
|
target="_blank"
|
||||||
<Result status="500" />
|
rel="noopener noreferrer"
|
||||||
<Title level={2}>Troubleshooting</Title>
|
>
|
||||||
<Button target="_blank" rel="noopener noreferrer" href="https://owncast.online/docs/troubleshooting/" icon={<LinkOutlined/>} type="primary">Read Troubleshoting</Button>
|
<LinkOutlined /> Learn more
|
||||||
</Col>
|
</a>
|
||||||
<Col xs={24} lg={12} style={{textAlign: 'center'}}>
|
|
||||||
<Result status="404" />
|
|
||||||
<Title level={2}>Documentation</Title>
|
|
||||||
<Button target="_blank" rel="noopener noreferrer" href="https://owncast.online/" icon={<LinkOutlined/>} type="primary">Read the Docs</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Divider />
|
|
||||||
<Title level={2}>Common tasks</Title>
|
|
||||||
<Row gutter={[16, 16]}>
|
|
||||||
{
|
|
||||||
questions.map(question => (
|
|
||||||
<Col xs={24} lg={12}>
|
|
||||||
<Card key={question.title}>
|
|
||||||
<Meta
|
|
||||||
avatar={question.icon}
|
|
||||||
title={question.title}
|
|
||||||
description={question.content}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</Row>
|
|
||||||
<Divider />
|
|
||||||
<Title level={2}>Other</Title>
|
|
||||||
<Row gutter={[16, 16]}>
|
|
||||||
{
|
|
||||||
otherResources.map(question => (
|
|
||||||
<Col xs={24} lg={12}>
|
|
||||||
<Card key={question.title}>
|
|
||||||
<Meta
|
|
||||||
avatar={question.icon}
|
|
||||||
title={question.title}
|
|
||||||
description={question.content}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</Row>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <CameraTwoTone style={{ fontSize: '24px' }} />,
|
||||||
|
title: 'I need help configuring my broadcasting software',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
href="https://owncast.online/docs/broadcasting/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<LinkOutlined /> Learn more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Html5TwoTone style={{ fontSize: '24px' }} />,
|
||||||
|
title: 'I want to embed my stream into another site',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<a href="https://owncast.online/docs/embed/" target="_blank" rel="noopener noreferrer">
|
||||||
|
<LinkOutlined /> Learn more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <EditTwoTone style={{ fontSize: '24px' }} />,
|
||||||
|
title: 'I want to customize my website',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<a href="https://owncast.online/docs/website/" target="_blank" rel="noopener noreferrer">
|
||||||
|
<LinkOutlined /> Learn more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <SlidersTwoTone style={{ fontSize: '24px' }} />,
|
||||||
|
title: 'I want to tweak my encoding quality or performance',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<a href="https://owncast.online/docs/encoding/" target="_blank" rel="noopener noreferrer">
|
||||||
|
<LinkOutlined /> Learn more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <DatabaseTwoTone style={{ fontSize: '24px' }} />,
|
||||||
|
title: 'I want to offload my video to an external storage provider',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<a href="https://owncast.online/docs/s3/" target="_blank" rel="noopener noreferrer">
|
||||||
|
<LinkOutlined /> Learn more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const otherResources = [
|
||||||
|
{
|
||||||
|
icon: <BugTwoTone style={{ fontSize: '24px' }} />,
|
||||||
|
title: 'I found a bug',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
If you found a bug, then please
|
||||||
|
<a
|
||||||
|
href="https://github.com/owncast/owncast/issues/new/choose"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
let us know
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <QuestionCircleTwoTone style={{ fontSize: '24px' }} />,
|
||||||
|
title: 'I have a general question',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
Most general questions are answered in our
|
||||||
|
<a href="https://owncast.online/docs/faq/" target="_blank" rel="noopener noreferrer">
|
||||||
|
{' '}
|
||||||
|
FAQ
|
||||||
|
</a>{' '}
|
||||||
|
or exist in our <a href="https://github.com/owncast/owncast/discussions">discussions</a>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ApiTwoTone style={{ fontSize: '24px' }} />,
|
||||||
|
title: 'I want to use the API',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
You can view the API documentation for either the
|
||||||
|
<a href="https://owncast.online/api/latest" target="_blank" rel="noopener noreferrer">
|
||||||
|
latest
|
||||||
|
</a>
|
||||||
|
or
|
||||||
|
<a
|
||||||
|
href="https://owncast.online/api/development"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
development
|
||||||
|
</a>
|
||||||
|
release.
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title style={{ textAlign: 'center' }}>How can we help you?</Title>
|
||||||
|
<Row gutter={[16, 16]} justify="space-around" align="middle">
|
||||||
|
<Col xs={24} lg={12} style={{ textAlign: 'center' }}>
|
||||||
|
<Result status="500" />
|
||||||
|
<Title level={2}>Troubleshooting</Title>
|
||||||
|
<Button
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://owncast.online/docs/troubleshooting/"
|
||||||
|
icon={<LinkOutlined />}
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
Read Troubleshoting
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} lg={12} style={{ textAlign: 'center' }}>
|
||||||
|
<Result status="404" />
|
||||||
|
<Title level={2}>Documentation</Title>
|
||||||
|
<Button
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://owncast.online/"
|
||||||
|
icon={<LinkOutlined />}
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
Read the Docs
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Divider />
|
||||||
|
<Title level={2}>Common tasks</Title>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{questions.map(question => (
|
||||||
|
<Col xs={24} lg={12}>
|
||||||
|
<Card key={question.title}>
|
||||||
|
<Meta avatar={question.icon} title={question.title} description={question.content} />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
<Divider />
|
||||||
|
<Title level={2}>Other</Title>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{otherResources.map(question => (
|
||||||
|
<Col xs={24} lg={12}>
|
||||||
|
<Card key={question.title}>
|
||||||
|
<Meta avatar={question.icon} title={question.title} description={question.content} />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,20 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react';
|
import React, { useState, useEffect, useContext } from 'react';
|
||||||
import { Table, Row } from "antd";
|
import { Table, Row } from 'antd';
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { UserOutlined} from "@ant-design/icons";
|
import { UserOutlined } from '@ant-design/icons';
|
||||||
import { SortOrder } from "antd/lib/table/interface";
|
import { SortOrder } from 'antd/lib/table/interface';
|
||||||
import Chart from "./components/chart";
|
import Chart from './components/chart';
|
||||||
import StatisticItem from "./components/statistic";
|
import StatisticItem from './components/statistic';
|
||||||
|
|
||||||
import { ServerStatusContext } from '../utils/server-status-context';
|
import { ServerStatusContext } from '../utils/server-status-context';
|
||||||
|
|
||||||
import {
|
import { CONNECTED_CLIENTS, VIEWERS_OVER_TIME, fetchData } from '../utils/apis';
|
||||||
CONNECTED_CLIENTS,
|
|
||||||
VIEWERS_OVER_TIME,
|
|
||||||
fetchData,
|
|
||||||
} from "../utils/apis";
|
|
||||||
|
|
||||||
const FETCH_INTERVAL = 60 * 1000; // 1 min
|
const FETCH_INTERVAL = 60 * 1000; // 1 min
|
||||||
|
|
||||||
export default function ViewersOverTime() {
|
export default function ViewersOverTime() {
|
||||||
const context = useContext(ServerStatusContext);
|
const context = useContext(ServerStatusContext);
|
||||||
const {
|
const { online, viewerCount, overallPeakViewerCount, sessionPeakViewerCount } = context || {};
|
||||||
online,
|
|
||||||
viewerCount,
|
|
||||||
overallPeakViewerCount,
|
|
||||||
sessionPeakViewerCount,
|
|
||||||
} = context || {};
|
|
||||||
|
|
||||||
const [viewerInfo, setViewerInfo] = useState([]);
|
const [viewerInfo, setViewerInfo] = useState([]);
|
||||||
const [clients, setClients] = useState([]);
|
const [clients, setClients] = useState([]);
|
||||||
@@ -33,14 +24,14 @@ export default function ViewersOverTime() {
|
|||||||
const result = await fetchData(VIEWERS_OVER_TIME);
|
const result = await fetchData(VIEWERS_OVER_TIME);
|
||||||
setViewerInfo(result);
|
setViewerInfo(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("==== error", error);
|
console.log('==== error', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await fetchData(CONNECTED_CLIENTS);
|
const result = await fetchData(CONNECTED_CLIENTS);
|
||||||
setClients(result);
|
setClients(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("==== error", error);
|
console.log('==== error', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,43 +53,43 @@ export default function ViewersOverTime() {
|
|||||||
// todo - check to see if broadcast active has changed. if so, start polling.
|
// todo - check to see if broadcast active has changed. if so, start polling.
|
||||||
|
|
||||||
if (!viewerInfo.length) {
|
if (!viewerInfo.length) {
|
||||||
return "no info";
|
return 'no info';
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: "User name",
|
title: 'User name',
|
||||||
dataIndex: "username",
|
dataIndex: 'username',
|
||||||
key: "username",
|
key: 'username',
|
||||||
render: (username) => username || "-",
|
render: username => username || '-',
|
||||||
sorter: (a, b) => a.username - b.username,
|
sorter: (a, b) => a.username - b.username,
|
||||||
sortDirections: ["descend", "ascend"] as SortOrder[],
|
sortDirections: ['descend', 'ascend'] as SortOrder[],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Messages sent",
|
title: 'Messages sent',
|
||||||
dataIndex: "messageCount",
|
dataIndex: 'messageCount',
|
||||||
key: "messageCount",
|
key: 'messageCount',
|
||||||
sorter: (a, b) => a.messageCount - b.messageCount,
|
sorter: (a, b) => a.messageCount - b.messageCount,
|
||||||
sortDirections: ["descend", "ascend"] as SortOrder[],
|
sortDirections: ['descend', 'ascend'] as SortOrder[],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Connected Time",
|
title: 'Connected Time',
|
||||||
dataIndex: "connectedAt",
|
dataIndex: 'connectedAt',
|
||||||
key: "connectedAt",
|
key: 'connectedAt',
|
||||||
render: (time) => formatDistanceToNow(new Date(time)),
|
render: time => formatDistanceToNow(new Date(time)),
|
||||||
sorter: (a, b) => new Date(a.connectedAt).getTime() - new Date(b.connectedAt).getTime(),
|
sorter: (a, b) => new Date(a.connectedAt).getTime() - new Date(b.connectedAt).getTime(),
|
||||||
sortDirections: ["descend", "ascend"] as SortOrder[],
|
sortDirections: ['descend', 'ascend'] as SortOrder[],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "User Agent",
|
title: 'User Agent',
|
||||||
dataIndex: "userAgent",
|
dataIndex: 'userAgent',
|
||||||
key: "userAgent",
|
key: 'userAgent',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Location",
|
title: 'Location',
|
||||||
dataIndex: "geo",
|
dataIndex: 'geo',
|
||||||
key: "geo",
|
key: 'geo',
|
||||||
render: (geo) => geo ? `${geo.regionName}, ${geo.countryCode}` : '-',
|
render: geo => (geo ? `${geo.regionName}, ${geo.countryCode}` : '-'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -122,7 +113,7 @@ export default function ViewersOverTime() {
|
|||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Chart title="Viewers" data={viewerInfo} color="#2087E2" unit="" />
|
<Chart title="Viewers" data={viewerInfo} color="#2087E2" unit="" />
|
||||||
<Table dataSource={clients} columns={columns} rowKey={(row) => row.clientID} />
|
<Table dataSource={clients} columns={columns} rowKey={row => row.clientID} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user