diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 2bd5ce96a6..2a1db88715 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -1,5 +1,9 @@ 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 ServerStatusProvider from '../utils/server-status-context'; diff --git a/web/pages/components/chart.tsx b/web/pages/components/chart.tsx index 5a6968f366..37176df10c 100644 --- a/web/pages/components/chart.tsx +++ b/web/pages/components/chart.tsx @@ -126,4 +126,6 @@ export default function Chart({ data, title, color, unit, dataCollections }: Cha Chart.defaultProps = { dataCollections: [], + data: [], + title: '', }; diff --git a/web/pages/components/statistic.tsx b/web/pages/components/statistic.tsx index 64910e443a..beefee6200 100644 --- a/web/pages/components/statistic.tsx +++ b/web/pages/components/statistic.tsx @@ -1,32 +1,25 @@ import { Typography, Statistic, Card, Col, Progress} from "antd"; const { Text } = Typography; -interface ItemProps { - title: string, - value: string, - prefix: JSX.Element, - color: string, +interface StatisticItemProps { + title?: string, + value?: any, + prefix?: JSX.Element, + // color?: string, 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}) { +function ProgressView({ title, value, prefix, color }: StatisticItemProps) { const endColor = value > 90 ? 'red' : color; const content = (
@@ -36,22 +29,43 @@ function ProgressView({title, value, prefix, color}) {
) return ( - content} /> + content} + /> ) } +ProgressView.defaultProps = defaultProps; -function StatisticView({title, value, prefix, color}) { - const valueStyle = { fontSize: "1.8rem" }; - +function StatisticView({ title, value, prefix }: StatisticItemProps) { return ( ) -} \ No newline at end of file +} +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 ( + +
+ +
+
+ ); +} +StatisticItem.defaultProps = defaultProps; diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 166e1a637b..b6bc589428 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ /* Will display an overview with the following datasources: 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"; -<<<<<<< HEAD -import { Row, Skeleton, Typography } from "antd"; -======= -import { Row, Col, Skeleton, Result, List, Typography, Card } from "antd"; ->>>>>>> 4cdf5b73baa0584a0e6b2f586c27ca53923c65c7 +import { Row, Col, Skeleton, Result, List, Typography, Card, Statistic } from "antd"; import { UserOutlined, ClockCircleOutlined } from "@ant-design/icons"; import { formatDistanceToNow, formatRelative } from "date-fns"; import { ServerStatusContext } from "../utils/server-status-context"; @@ -21,264 +18,182 @@ import LogTable from "./components/log-table"; import Offline from './offline-notice'; import { - STATUS, SERVER_CONFIG, LOGS_WARN, fetchData, FETCH_INTERVAL, } from "../utils/apis"; import { formatIPAddress, isEmptyObject } from "../utils/format"; +import { INITIAL_SERVER_CONFIG_STATE } from "./update-server-config"; const { Title } = Typography; -<<<<<<< HEAD -======= ->>>>>>> 4cdf5b73baa0584a0e6b2f586c27ca53923c65c7 - -<<<<<<< HEAD export default function Home() { - const context = useContext(BroadcastStatusContext); -======= -export default function Stats() { - const context = useContext(ServerStatusContext); ->>>>>>> ca90d28ec1d0a0f0059a4649dd00fb95b9d4fa3d - const { broadcaster } = context || {}; + const serverStatusData = useContext(ServerStatusContext); + const { broadcaster } = serverStatusData || {}; 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. - const [config, setConfig] = useState({ - streamKey: "", - yp: { - enabled: false, - }, - videoSettings: { - videoQualityVariants: [ - { - audioPassthrough: false, - videoBitrate: 0, - audioBitrate: 0, - framerate: 0, - }, - ], - }, - }); - const [logs, setLogs] = useState([]); - + const [configData, setServerConfig] = useState(INITIAL_SERVER_CONFIG_STATE); const getConfig = async () => { try { const result = await fetchData(SERVER_CONFIG); - setConfig(result); + setServerConfig(result); + console.log("CONFIG", result); } catch (error) { console.log(error); } }; + const [logsData, setLogs] = useState([]); const getLogs = async () => { try { const result = await fetchData(LOGS_WARN); setLogs(result); + console.log("LOGS", result); } catch (error) { console.log("==== error", error); } }; - + const getMoreStats = () => { + getLogs(); + getConfig(); + } useEffect(() => { - setInterval(getStats, FETCH_INTERVAL); - getStats(); + let intervalId = null; + intervalId = setInterval(getMoreStats, FETCH_INTERVAL); + return () => { + clearInterval(intervalId); + } }, []); - if (isEmptyObject(config) || isEmptyObject(stats)) { + if (isEmptyObject(configData) || isEmptyObject(serverStatusData)) { return ( -
+ <> -
+ ); } - const logTable = logs.length > 0 ? : null - console.log(logs) - if (!broadcaster) { - return ; + return ; } - const videoSettings = config.videoSettings.videoQualityVariants; - const videoQualitySettings = videoSettings.map((setting) => { + // map out settings + const videoQualitySettings = configData?.videoSettings?.videoQualityVariants?.map((setting, index) => { + const { audioPassthrough, audioBitrate, videoBitrate, framerate } = setting; const audioSetting = - setting.audioPassthrough || setting.audioBitrate === 0 + audioPassthrough || audioBitrate === 0 ? `${streamDetails.audioBitrate} kpbs (passthrough)` - : `${setting.audioBitrate} kbps`; + : `${audioBitrate} kbps`; + let settingTitle = 'Outbound Stream Details'; + settingTitle = (videoQualitySettings?.length > 1) ? + `${settingTitle} ${index + 1}` : settingTitle; return ( - + - + ); }); - const { viewerCount, sessionMaxViewerCount } = stats; + const { viewerCount, sessionMaxViewerCount } = serverStatusData; 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 ( -
- Server Overview - - } - color="#334" - /> - } - color="#334" - /> - } - color="#334" - /> - +
+ Stream Overview - - - - - +
- {videoQualitySettings} - - - - +
+ + } + /> + } + /> + } + /> + +
- {logTable} +
+ +
+ {videoQualitySettings} +
+ +
+ + + + + + +
+ + + + +
+
+
+
+ + {logsData.length ? ( + <> + Stream Logs + + + ): null}
); - - function Offline() { - const data = [ - { - title: "Send some test content", - content: ( -
- Test your server with any video you have around. Pass it to the test script and start streaming it. -
- ./test/ocTestStream.sh yourVideo.mp4 -
-
- ), - }, - { - title: "Use your broadcasting software", - content: ( - - ) - }, - { - 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: ( - - ) - } - ]; - return ( -
- } - title="No stream is active." - subTitle="You should start one." - /> - - ( - - {item.content} - - )} - /> - {logTable} -
- ); - } } diff --git a/web/pages/offline-notice.tsx b/web/pages/offline-notice.tsx index 85bb10f5ea..0da427e635 100644 --- a/web/pages/offline-notice.tsx +++ b/web/pages/offline-notice.tsx @@ -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 LogTable from "./components/log-table"; -export default function Offline() { + +const { Title } = Typography; +const { Meta } = Card; + +export default function Offline({ logs = [] }) { const data = [ { + icon: , title: "Send some test content", content: (
Test your server with any video you have around. Pass it to the test script and start streaming it. -
- ./test/ocTestStream.sh yourVideo.mp4 -
+
+            ./test/ocTestStream.sh yourVideo.mp4
+          
), }, { + icon: , title: "Use your broadcasting software", content: (
@@ -23,10 +31,12 @@ export default function Offline() { ) }, { + icon: , title: "Chat is disabled", content: "Chat will continue to be disabled until you begin a live stream." }, { + icon: , title: "Embed your video onto other sites", content: (
@@ -35,32 +45,37 @@ export default function Offline() { ) } ]; - return ( -
- } - title="No stream is active." - subTitle="You should start one." - /> - ( - - {item.content} - - )} - /> - {logTable} + return ( +
+
+ } + title="No stream is active." + subTitle="You should start one." + /> +
+
+ { + data.map(item => ( + + + + )) + } +
+ + + {logs.length ? ( + <> + Stream Logs + + + ): null}
); } diff --git a/web/pages/update-server-config.tsx b/web/pages/update-server-config.tsx index 4527efda2b..34e9053667 100644 --- a/web/pages/update-server-config.tsx +++ b/web/pages/update-server-config.tsx @@ -8,6 +8,22 @@ import KeyValueTable from "./components/key-value-table"; const { Title } = Typography; 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 }) { if (!config) { @@ -121,12 +137,12 @@ function PageContent({ config }) { } export default function ServerConfig() { - const [config, setConfig] = useState({}); + const [config, setConfig] = useState(INITIAL_SERVER_CONFIG_STATE); const getInfo = async () => { try { const result = await fetchData(SERVER_CONFIG); - console.log("viewers result", result) + console.log("SERVER_CONFIG", result) setConfig({ ...result }); @@ -134,18 +150,9 @@ export default function ServerConfig() { setConfig({ ...config, message: error.message }); } }; - - useEffect(() => { - let getStatusIntervalId = null; - getInfo(); - getStatusIntervalId = setInterval(getInfo, FETCH_INTERVAL); + getInfo(); - // returned function will be called on component unmount - return () => { - clearInterval(getStatusIntervalId); - } - }, []); return (
diff --git a/web/styles/colors.scss b/web/styles/colors.scss new file mode 100644 index 0000000000..22aa1d407e --- /dev/null +++ b/web/styles/colors.scss @@ -0,0 +1,6 @@ +:root { + --owncast-purple: rgba(90,103,216,1); + --owncast-purple-highlight: #ccd; + + --online-color: #73dd3f; +} diff --git a/web/styles/globals.scss b/web/styles/globals.scss index 7583cbbf0e..b96c1b98e4 100644 --- a/web/styles/globals.scss +++ b/web/styles/globals.scss @@ -1,4 +1,4 @@ -$owncast-purple: rgba(90,103,216,1);; +$owncast-purple: rgba(90,103,216,1); html, body { @@ -19,6 +19,12 @@ a { 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 { background-color: $owncast-purple; @@ -31,4 +37,4 @@ a { .recharts-wrapper { font-size: 12px; -} \ No newline at end of file +} diff --git a/web/styles/home.scss b/web/styles/home.scss new file mode 100644 index 0000000000..ea41587ec3 --- /dev/null +++ b/web/styles/home.scss @@ -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% + } + + } +} diff --git a/web/styles/styles.module.css b/web/styles/styles.module.css index 07702e59c6..a35cc5df56 100644 --- a/web/styles/styles.module.css +++ b/web/styles/styles.module.css @@ -61,10 +61,10 @@ color: #999; } .online .statusIcon svg { - fill: #52c41a; + fill: var(--online-color) } .online .statusLabel { - color: #52c41a; + color: var(--online-color) } diff --git a/web/utils/server-status-context.tsx b/web/utils/server-status-context.tsx index f0c63aa430..9fb81ec994 100644 --- a/web/utils/server-status-context.tsx +++ b/web/utils/server-status-context.tsx @@ -8,6 +8,7 @@ const initialState = { broadcaster: null, online: false, viewerCount: 0, + sessionMaxViewerCount: 0, sessionPeakViewerCount: 0, overallPeakViewerCount: 0, disableUpgradeChecks: true, @@ -25,7 +26,7 @@ const ServerStatusProvider = ({ children }) => { setStatus({ ...result }); } catch (error) { - // setBroadcasterStatus({ ...broadcasterStatus, message: error.message }); + // todo } };