clean up of home section; now with styling

This commit is contained in:
gingervitis
2020-11-13 03:43:28 -08:00
parent 3f1f96a768
commit 2211572ba1
11 changed files with 409 additions and 279 deletions

View File

@@ -1,5 +1,9 @@
import 'antd/dist/antd.compact.css'; import 'antd/dist/antd.compact.css';
import "../styles/globals.scss"; import '../styles/colors.scss';
import '../styles/globals.scss';
// GW: I can't override ant design styles through components using NextJS's built-in CSS modules. So I'll just import styles here for now and figure out enabling SASS modules later.
import '../styles/home.scss';
import { AppProps } from 'next/app'; import { AppProps } from 'next/app';
import ServerStatusProvider from '../utils/server-status-context'; import ServerStatusProvider from '../utils/server-status-context';

View File

@@ -126,4 +126,6 @@ export default function Chart({ data, title, color, unit, dataCollections }: Cha
Chart.defaultProps = { Chart.defaultProps = {
dataCollections: [], dataCollections: [],
data: [],
title: '',
}; };

View File

@@ -1,32 +1,25 @@
import { Typography, Statistic, Card, Col, Progress} from "antd"; import { Typography, Statistic, Card, Col, Progress} from "antd";
const { Text } = Typography; const { Text } = Typography;
interface ItemProps { interface StatisticItemProps {
title: string, title?: string,
value: string, value?: any,
prefix: JSX.Element, prefix?: JSX.Element,
color: string, // color?: string,
progress?: boolean, progress?: boolean,
centered: boolean, centered?: boolean,
};
const defaultProps = {
title: '',
value: 0,
prefix: null,
// color: '',
progress: false,
centered: false,
}; };
export default function StatisticItem(props: ItemProps) {
const { title, value, prefix } = props;
const View = props.progress ? ProgressView : StatisticView;
const style = props.centered ? {display: 'flex', alignItems: 'center', justifyContent: 'center'} : {};
return ( function ProgressView({ title, value, prefix, color }: StatisticItemProps) {
<Col span={8}>
<Card>
<div style={style}>
<View {...props} />
</div>
</Card>
</Col>
);
}
function ProgressView({title, value, prefix, color}) {
const endColor = value > 90 ? 'red' : color; const endColor = value > 90 ? 'red' : color;
const content = ( const content = (
<div> <div>
@@ -36,22 +29,43 @@ function ProgressView({title, value, prefix, color}) {
</div> </div>
) )
return ( return (
<Progress type="dashboard" percent={value} width={120} strokeColor={{ <Progress
type="dashboard"
percent={value}
width={120}
strokeColor={{
'0%': color, '0%': color,
'90%': endColor, '90%': endColor,
}} format={percent => content} /> }}
format={percent => content}
/>
) )
} }
ProgressView.defaultProps = defaultProps;
function StatisticView({title, value, prefix, color}) { function StatisticView({ title, value, prefix }: StatisticItemProps) {
const valueStyle = { fontSize: "1.8rem" };
return ( return (
<Statistic <Statistic
title={title} title={title}
value={value} value={value}
valueStyle={valueStyle}
prefix={prefix} prefix={prefix}
/> />
) )
} }
StatisticView.defaultProps = defaultProps;
export default function StatisticItem(props: StatisticItemProps) {
const { progress, centered } = props;
const View = progress ? ProgressView : StatisticView;
const style = centered ? {display: 'flex', alignItems: 'center', justifyContent: 'center'} : {};
return (
<Card type="inner">
<div style={style}>
<View {...props} />
</div>
</Card>
);
}
StatisticItem.defaultProps = defaultProps;

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-console */
/* /*
Will display an overview with the following datasources: Will display an overview with the following datasources:
1. Current broadcaster. 1. Current broadcaster.
@@ -8,11 +9,7 @@ TODO: Link each overview value to the sub-page that focuses on it.
*/ */
import React, { useState, useEffect, useContext } from "react"; import React, { useState, useEffect, useContext } from "react";
<<<<<<< HEAD import { Row, Col, Skeleton, Result, List, Typography, Card, Statistic } from "antd";
import { Row, Skeleton, Typography } from "antd";
=======
import { Row, Col, Skeleton, Result, List, Typography, Card } from "antd";
>>>>>>> 4cdf5b73baa0584a0e6b2f586c27ca53923c65c7
import { UserOutlined, ClockCircleOutlined } from "@ant-design/icons"; import { UserOutlined, ClockCircleOutlined } from "@ant-design/icons";
import { formatDistanceToNow, formatRelative } from "date-fns"; import { formatDistanceToNow, formatRelative } from "date-fns";
import { ServerStatusContext } from "../utils/server-status-context"; import { ServerStatusContext } from "../utils/server-status-context";
@@ -21,264 +18,182 @@ import LogTable from "./components/log-table";
import Offline from './offline-notice'; import Offline from './offline-notice';
import { import {
STATUS,
SERVER_CONFIG, SERVER_CONFIG,
LOGS_WARN, LOGS_WARN,
fetchData, fetchData,
FETCH_INTERVAL, FETCH_INTERVAL,
} from "../utils/apis"; } from "../utils/apis";
import { formatIPAddress, isEmptyObject } from "../utils/format"; import { formatIPAddress, isEmptyObject } from "../utils/format";
import { INITIAL_SERVER_CONFIG_STATE } from "./update-server-config";
const { Title } = Typography; const { Title } = Typography;
<<<<<<< HEAD
=======
>>>>>>> 4cdf5b73baa0584a0e6b2f586c27ca53923c65c7
<<<<<<< HEAD
export default function Home() { export default function Home() {
const context = useContext(BroadcastStatusContext); const serverStatusData = useContext(ServerStatusContext);
======= const { broadcaster } = serverStatusData || {};
export default function Stats() {
const context = useContext(ServerStatusContext);
>>>>>>> ca90d28ec1d0a0f0059a4649dd00fb95b9d4fa3d
const { broadcaster } = context || {};
const { remoteAddr, streamDetails } = broadcaster || {}; const { remoteAddr, streamDetails } = broadcaster || {};
// Pull in the server status so we can show server overview.
const [stats, setStats] = useState(null);
const getStats = async () => {
try {
const result = await fetchData(STATUS);
setStats(result);
} catch (error) {
console.log(error);
}
getConfig();
getLogs();
};
// Pull in the server config so we can show config overview. // Pull in the server config so we can show config overview.
const [config, setConfig] = useState({ const [configData, setServerConfig] = useState(INITIAL_SERVER_CONFIG_STATE);
streamKey: "",
yp: {
enabled: false,
},
videoSettings: {
videoQualityVariants: [
{
audioPassthrough: false,
videoBitrate: 0,
audioBitrate: 0,
framerate: 0,
},
],
},
});
const [logs, setLogs] = useState([]);
const getConfig = async () => { const getConfig = async () => {
try { try {
const result = await fetchData(SERVER_CONFIG); const result = await fetchData(SERVER_CONFIG);
setConfig(result); setServerConfig(result);
console.log("CONFIG", result);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
}; };
const [logsData, setLogs] = useState([]);
const getLogs = async () => { const getLogs = async () => {
try { try {
const result = await fetchData(LOGS_WARN); const result = await fetchData(LOGS_WARN);
setLogs(result); setLogs(result);
console.log("LOGS", result);
} catch (error) { } catch (error) {
console.log("==== error", error); console.log("==== error", error);
} }
}; };
const getMoreStats = () => {
getLogs();
getConfig();
}
useEffect(() => { useEffect(() => {
setInterval(getStats, FETCH_INTERVAL); let intervalId = null;
getStats(); intervalId = setInterval(getMoreStats, FETCH_INTERVAL);
return () => {
clearInterval(intervalId);
}
}, []); }, []);
if (isEmptyObject(config) || isEmptyObject(stats)) { if (isEmptyObject(configData) || isEmptyObject(serverStatusData)) {
return ( return (
<div> <>
<Skeleton active /> <Skeleton active />
<Skeleton active /> <Skeleton active />
<Skeleton active /> <Skeleton active />
</div> </>
); );
} }
const logTable = logs.length > 0 ? <LogTable logs={logs} pageSize={5} /> : null
console.log(logs)
if (!broadcaster) { if (!broadcaster) {
return <Offline />; return <Offline logs={logsData} />;
} }
const videoSettings = config.videoSettings.videoQualityVariants; // map out settings
const videoQualitySettings = videoSettings.map((setting) => { const videoQualitySettings = configData?.videoSettings?.videoQualityVariants?.map((setting, index) => {
const { audioPassthrough, audioBitrate, videoBitrate, framerate } = setting;
const audioSetting = const audioSetting =
setting.audioPassthrough || setting.audioBitrate === 0 audioPassthrough || audioBitrate === 0
? `${streamDetails.audioBitrate} kpbs (passthrough)` ? `${streamDetails.audioBitrate} kpbs (passthrough)`
: `${setting.audioBitrate} kbps`; : `${audioBitrate} kbps`;
let settingTitle = 'Outbound Stream Details';
settingTitle = (videoQualitySettings?.length > 1) ?
`${settingTitle} ${index + 1}` : settingTitle;
return ( return (
<Row gutter={[16, 16]} key={`setting-${setting.videoBitrate}`}> <Card title={settingTitle} type="inner">
<StatisticItem <StatisticItem
title="Outbound Video Stream" title="Outbound Video Stream"
value={`${setting.videoBitrate} kbps ${setting.framerate} fps`} value={`${videoBitrate} kbps, ${framerate} fps`}
prefix={null} prefix={null}
color="#334"
/> />
<StatisticItem <StatisticItem
title="Outbound Audio Stream" title="Outbound Audio Stream"
value={audioSetting} value={audioSetting}
prefix={null} prefix={null}
color="#334"
/> />
</Row> </Card>
); );
}); });
const { viewerCount, sessionMaxViewerCount } = stats; const { viewerCount, sessionMaxViewerCount } = serverStatusData;
const streamVideoDetailString = `${streamDetails.videoCodec} ${streamDetails.videoBitrate} kbps ${streamDetails.width}x${streamDetails.height}`; const streamVideoDetailString = `${streamDetails.videoCodec} ${streamDetails.videoBitrate} kbps ${streamDetails.width}x${streamDetails.height}`;
const streamAudioDetailString = `${streamDetails.audioCodec} ${streamDetails.audioBitrate} kpbs`; const streamAudioDetailString = `${streamDetails.audioCodec} ${streamDetails.audioBitrate} kbps`;
const broadcastDate = new Date(broadcaster.time);
return ( return (
<div> <div className="home-container">
<Title>Server Overview</Title> <Title>Stream Overview</Title>
<Row gutter={[16, 16]}>
<StatisticItem <div className="sections-container">
<div className="section online-status-section">
<Card title="Stream is online" type="inner">
<Statistic
title={`Stream started ${formatRelative( title={`Stream started ${formatRelative(
new Date(broadcaster.time), broadcastDate,
new Date() Date.now()
)}`} )}`}
value={formatDistanceToNow(new Date(broadcaster.time))} value={formatDistanceToNow(broadcastDate)}
prefix={<ClockCircleOutlined />} prefix={<ClockCircleOutlined />}
color="#334"
/> />
<StatisticItem <Statistic
title="Viewers" title="Viewers"
value={viewerCount} value={viewerCount}
prefix={<UserOutlined />} prefix={<UserOutlined />}
color="#334"
/> />
<StatisticItem <Statistic
title="Peak viewer count" title="Peak viewer count"
value={sessionMaxViewerCount} value={sessionMaxViewerCount}
prefix={<UserOutlined />} prefix={<UserOutlined />}
color="#334"
/> />
</Row> </Card>
</div>
<Row gutter={[16, 16]}> <div className="section stream-details-section">
<div className="details outbound-details">
{videoQualitySettings}
</div>
<div className="details other-details">
<Card title="Inbound Stream Details" type="inner">
<StatisticItem <StatisticItem
title="Input" title="Input"
value={formatIPAddress(remoteAddr)} value={formatIPAddress(remoteAddr)}
prefix={null} prefix={null}
color="#334"
/> />
<StatisticItem <StatisticItem
title="Inbound Video Stream" title="Inbound Video Stream"
value={streamVideoDetailString} value={streamVideoDetailString}
prefix={null} prefix={null}
color="#334"
/> />
<StatisticItem <StatisticItem
title="Inbound Audio Stream" title="Inbound Audio Stream"
value={streamAudioDetailString} value={streamAudioDetailString}
prefix={null} prefix={null}
color="#334"
/> />
</Row> </Card>
{videoQualitySettings} <div className="server-detail">
<Card title="Server Config" type="inner">
<Row gutter={[16, 16]}>
<StatisticItem <StatisticItem
title="Stream key" title="Stream key"
value={config.streamKey} value={configData.streamKey}
prefix={null} prefix={null}
color="#334"
/> />
<StatisticItem <StatisticItem
title="Directory registration enabled" title="Directory registration enabled"
value={config.yp.enabled.toString()} value={configData.yp.enabled.toString()}
prefix={null} prefix={null}
color="#334"
/> />
</Row> </Card>
</div>
</div>
</div>
</div>
{logTable} {logsData.length ? (
</div> <>
); <Title level={2}>Stream Logs</Title>
<LogTable logs={logsData} pageSize={5} />
function Offline() { </>
const data = [ ): null}
{
title: "Send some test content",
content: (
<div>
Test your server with any video you have around. Pass it to the test script and start streaming it.
<blockquote>
<em>./test/ocTestStream.sh yourVideo.mp4</em>
</blockquote>
</div>
),
},
{
title: "Use your broadcasting software",
content: (
<div>
<a href="https://owncast.online/docs/broadcasting/">Learn how to point your existing software to your new server and start streaming your content.</a>
</div>
)
},
{
title: "Chat is disabled",
content: "Chat will continue to be disabled until you begin a live stream."
},
{
title: "Embed your video onto other sites",
content: (
<div>
<a href="https://owncast.online/docs/embed">Learn how you can add your Owncast stream to other sites you control.</a>
</div>
)
}
];
return (
<div>
<Result
icon={<OwncastLogo />}
title="No stream is active."
subTitle="You should start one."
/>
<List
grid={{
gutter: 16,
xs: 1,
sm: 2,
md: 2,
lg: 6,
xl: 3,
xxl: 3,
}}
dataSource={data}
renderItem={(item) => (
<List.Item>
<Card title={item.title}>{item.content}</Card>
</List.Item>
)}
/>
{logTable}
</div> </div>
); );
} }
}

View File

@@ -1,20 +1,28 @@
import { Result, List, Card } from "antd"; import { Result, Card, Typography } from "antd";
import { MessageTwoTone, BulbTwoTone, BookTwoTone, PlaySquareTwoTone } from '@ant-design/icons';
import OwncastLogo from "./components/logo" import OwncastLogo from "./components/logo"
import LogTable from "./components/log-table";
export default function Offline() {
const { Title } = Typography;
const { Meta } = Card;
export default function Offline({ logs = [] }) {
const data = [ const data = [
{ {
icon: <BulbTwoTone twoToneColor="#ffd33d" />,
title: "Send some test content", title: "Send some test content",
content: ( content: (
<div> <div>
Test your server with any video you have around. Pass it to the test script and start streaming it. Test your server with any video you have around. Pass it to the test script and start streaming it.
<blockquote> <pre>
<em>./test/ocTestStream.sh yourVideo.mp4</em> <code>./test/ocTestStream.sh yourVideo.mp4</code>
</blockquote> </pre>
</div> </div>
), ),
}, },
{ {
icon: <BookTwoTone twoToneColor="#6f42c1" />,
title: "Use your broadcasting software", title: "Use your broadcasting software",
content: ( content: (
<div> <div>
@@ -23,10 +31,12 @@ export default function Offline() {
) )
}, },
{ {
icon: <MessageTwoTone twoToneColor="#0366d6" />,
title: "Chat is disabled", title: "Chat is disabled",
content: "Chat will continue to be disabled until you begin a live stream." content: "Chat will continue to be disabled until you begin a live stream."
}, },
{ {
icon: <PlaySquareTwoTone twoToneColor="#f9826c" />,
title: "Embed your video onto other sites", title: "Embed your video onto other sites",
content: ( content: (
<div> <div>
@@ -35,32 +45,37 @@ export default function Offline() {
) )
} }
]; ];
return ( return (
<div> <div className="offline-content">
<div className="logo-section">
<Result <Result
icon={<OwncastLogo />} icon={<OwncastLogo />}
title="No stream is active." title="No stream is active."
subTitle="You should start one." subTitle="You should start one."
/> />
</div>
<List <div className="list-section">
grid={{ {
gutter: 16, data.map(item => (
xs: 1, <Card key={item.title}>
sm: 2, <Meta
md: 2, avatar={item.icon}
lg: 6, title={item.title}
xl: 3, description={item.content}
xxl: 3,
}}
dataSource={data}
renderItem={(item) => (
<List.Item>
<Card title={item.title}>{item.content}</Card>
</List.Item>
)}
/> />
{logTable} </Card>
))
}
</div>
{logs.length ? (
<>
<Title level={2}>Stream Logs</Title>
<LogTable logs={logs} pageSize={5} />
</>
): null}
</div> </div>
); );
} }

View File

@@ -8,6 +8,22 @@ import KeyValueTable from "./components/key-value-table";
const { Title } = Typography; const { Title } = Typography;
const { TextArea } = Input; const { TextArea } = Input;
export const INITIAL_SERVER_CONFIG_STATE = {
streamKey: '',
yp: {
enabled: false,
},
videoSettings: {
videoQualityVariants: [
{
audioPassthrough: false,
videoBitrate: 0,
audioBitrate: 0,
framerate: 0,
},
],
}
};
function SocialHandles({ config }) { function SocialHandles({ config }) {
if (!config) { if (!config) {
@@ -121,12 +137,12 @@ function PageContent({ config }) {
} }
export default function ServerConfig() { export default function ServerConfig() {
const [config, setConfig] = useState({}); const [config, setConfig] = useState(INITIAL_SERVER_CONFIG_STATE);
const getInfo = async () => { const getInfo = async () => {
try { try {
const result = await fetchData(SERVER_CONFIG); const result = await fetchData(SERVER_CONFIG);
console.log("viewers result", result) console.log("SERVER_CONFIG", result)
setConfig({ ...result }); setConfig({ ...result });
@@ -135,17 +151,8 @@ export default function ServerConfig() {
} }
}; };
useEffect(() => {
let getStatusIntervalId = null;
getInfo(); getInfo();
getStatusIntervalId = setInterval(getInfo, FETCH_INTERVAL);
// returned function will be called on component unmount
return () => {
clearInterval(getStatusIntervalId);
}
}, []);
return ( return (
<div> <div>

6
web/styles/colors.scss Normal file
View File

@@ -0,0 +1,6 @@
:root {
--owncast-purple: rgba(90,103,216,1);
--owncast-purple-highlight: #ccd;
--online-color: #73dd3f;
}

View File

@@ -1,4 +1,4 @@
$owncast-purple: rgba(90,103,216,1);; $owncast-purple: rgba(90,103,216,1);
html, html,
body { body {
@@ -19,6 +19,12 @@ a {
box-sizing: border-box; box-sizing: border-box;
} }
pre {
display: block;
padding: 1rem;
margin: .5rem 0;
background-color: #eee;
}
.owncast-layout .ant-menu-dark.ant-menu-dark:not(.ant-menu-horizontal) .ant-menu-item-selected { .owncast-layout .ant-menu-dark.ant-menu-dark:not(.ant-menu-horizontal) .ant-menu-item-selected {
background-color: $owncast-purple; background-color: $owncast-purple;

160
web/styles/home.scss Normal file
View File

@@ -0,0 +1,160 @@
.home-container {
max-width: 1000px;
.section {
margin: 1rem 0;
}
.online-status-section {
> .ant-card {
box-shadow: 0px 1px 1px 0px rgba(0, 22, 40, 0.1);
}
.ant-card-head {
background-color: var(--owncast-purple);
border-color: #ccc;
color:#fff;
}
.ant-card-head-title {
font-size: .88rem;
}
.ant-statistic-title {
font-size: .88rem;
}
.ant-card-body {
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
.ant-statistic {
width: 30%;
text-align: center;
margin: 0 1rem;
}
}
}
.stream-details-section {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
width: 100%;
.details {
width: 49%;
> .ant-card {
margin-bottom: 1rem;
}
.ant-card-head {
background-color: #ccd;
color: black;
}
}
.server-detail {
.ant-card-body {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
.ant-card {
width: 49%;
}
}
.ant-card-head {
background-color: #669;
color: #fff;
}
}
}
@media (max-width: 800px) {
.online-status-section{
.ant-card-body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
.ant-statistic {
width: auto;
text-align: left;
margin: 1em;
}
}
}
.stream-details-section {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
width: 100%;
.details {
width: 100%;
}
}
}
}
.offline-content {
max-width: 1000px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
width: 100%;
.logo-section {
width: 50%;
.ant-result-title {
font-size: 2rem;
}
.ant-result-subtitle {
font-size: 1rem;
}
.ant-result-icon svg {
height: 8rem;
width: 8rem;
}
}
.list-section {
width: 50%;
> .ant-card {
margin-bottom: 1rem;
.ant-card-head {
background-color: #dde;
}
.ant-card-head-title {
font-size: 1rem;
}
.ant-card-meta-avatar {
margin-top: .25rem;
svg {
height: 1.25rem;
width: 1.25rem;
}
}
.ant-card-body {
font-size: .88rem;
}
}
}
@media (max-width: 800px) {
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
.logo-section,
.list-section {
width: 100%
}
}
}

View File

@@ -61,10 +61,10 @@
color: #999; color: #999;
} }
.online .statusIcon svg { .online .statusIcon svg {
fill: #52c41a; fill: var(--online-color)
} }
.online .statusLabel { .online .statusLabel {
color: #52c41a; color: var(--online-color)
} }

View File

@@ -8,6 +8,7 @@ const initialState = {
broadcaster: null, broadcaster: null,
online: false, online: false,
viewerCount: 0, viewerCount: 0,
sessionMaxViewerCount: 0,
sessionPeakViewerCount: 0, sessionPeakViewerCount: 0,
overallPeakViewerCount: 0, overallPeakViewerCount: 0,
disableUpgradeChecks: true, disableUpgradeChecks: true,
@@ -25,7 +26,7 @@ const ServerStatusProvider = ({ children }) => {
setStatus({ ...result }); setStatus({ ...result });
} catch (error) { } catch (error) {
// setBroadcasterStatus({ ...broadcasterStatus, message: error.message }); // todo
} }
}; };