Merge branch 'master' of github.com:owncast/owncast-admin

This commit is contained in:
gingervitis
2020-11-12 22:39:18 -08:00
15 changed files with 220 additions and 114 deletions

View File

@@ -1,3 +1 @@
NEXT_PUBLIC_ADMIN_USERNAME=admin
NEXT_PUBLIC_ADMIN_STREAMKEY=abc123
NEXT_PUBLIC_API_HOST=/ NEXT_PUBLIC_API_HOST=/

View File

@@ -1,3 +1,4 @@
module.exports = { module.exports = {
basePath: "/admin", basePath: "/admin",
trailingSlash: true,
}; };

View File

@@ -5,7 +5,7 @@ import styles from '../../styles/styles.module.css';
interface ToolTipProps { interface ToolTipProps {
active?: boolean, active?: boolean,
payload?: object, payload?: {name: string, payload: {value: string, time: Date}}[],
unit?: string unit?: string
} }
@@ -22,6 +22,7 @@ interface TimedValue {
interface ChartProps { interface ChartProps {
data?: TimedValue[], data?: TimedValue[],
title?: string,
color: string, color: string,
unit: string, unit: string,
dataCollections?: any[], dataCollections?: any[],
@@ -31,19 +32,24 @@ function CustomizedTooltip(props: ToolTipProps) {
const { active, payload, unit } = props; const { active, payload, unit } = props;
if (active && payload && payload[0]) { if (active && payload && payload[0]) {
const time = payload[0].payload ? timeFormat("%I:%M")(new Date(payload[0].payload.time)) : ""; const time = payload[0].payload ? timeFormat("%I:%M")(new Date(payload[0].payload.time)) : "";
const tooltipDetails = payload.map(data => {
return <div className="label" key={data.name}>
{data.payload.value}{unit} {data.name}
</div>
});
return ( return (
<div className="custom-tooltip"> <span className="custom-tooltip">
<p className="label"> <strong>{time}</strong>
<strong>{time}</strong> {payload[0].payload.value} {unit} {tooltipDetails}
</p> </span>
</div>
); );
} }
return null; return null;
} }
CustomizedTooltip.defaultProps = defaultProps; CustomizedTooltip.defaultProps = defaultProps;
export default function Chart({ data, color, unit, dataCollections }: ChartProps) { export default function Chart({ data, title, color, unit, dataCollections }: ChartProps) {
if (!data && !dataCollections) { if (!data && !dataCollections) {
return null; return null;
} }
@@ -67,6 +73,18 @@ export default function Chart({ data, color, unit, dataCollections }: ChartProps
}); });
} }
const line = data ? (
<Line
type="natural"
dataKey="value"
stroke={color}
dot={null}
strokeWidth={3}
legendType="square"
name={title}
/>
) : null;
return ( return (
<div className={styles.lineChartContainer}> <div className={styles.lineChartContainer}>
<LineChart width={chartWidth} height={chartHeight} data={data}> <LineChart width={chartWidth} height={chartHeight} data={data}>
@@ -87,23 +105,18 @@ export default function Chart({ data, color, unit, dataCollections }: ChartProps
/> />
<Tooltip content={<CustomizedTooltip unit={unit} />} /> <Tooltip content={<CustomizedTooltip unit={unit} />} />
<Legend /> <Legend />
<Line {line}
type="monotone"
dataKey="value"
stroke={color}
dot={null}
strokeWidth={3}
/>
{dataCollections?.map((s) => ( {dataCollections?.map((s) => (
<Line <Line
dataKey="value" dataKey="value"
data={s.data} data={s.data}
name={s.name} name={s.name}
key={s.name} key={s.name}
type="monotone" type="natural"
stroke={s.color} stroke={s.color}
dot={null} dot={null}
strokeWidth={3} strokeWidth={3}
legendType="square"
/> />
))} ))}
</LineChart> </LineChart>

View File

@@ -77,7 +77,6 @@ export default function LogTable({ logs, pageSize }: Props) {
rowKey={(row) => row.time} rowKey={(row) => row.time}
pagination={{ pageSize: pageSize || 20 }} pagination={{ pageSize: pageSize || 20 }}
/> />
;
</div> </div>
); );
} }

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import Link from 'next/link'; import Link from 'next/link';
import { differenceInSeconds } from "date-fns"; import { differenceInSeconds } from "date-fns";
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { Layout, Menu } from 'antd'; import { Layout, Menu, Popover } from 'antd';
import { import {
SettingOutlined, SettingOutlined,
@@ -38,6 +38,11 @@ export default function MainLayout(props) {
const streamDurationString = online ? parseSecondsToDurationString(differenceInSeconds(new Date(), new Date(broadcaster.time))) : ""; const streamDurationString = online ? parseSecondsToDurationString(differenceInSeconds(new Date(), new Date(broadcaster.time))) : "";
const content = (
<div>
<img src="/thumbnail.jpg" width="200px" />
</div>
);
const statusIcon = online ? <PlayCircleFilled /> : <MinusSquareFilled />; const statusIcon = online ? <PlayCircleFilled /> : <MinusSquareFilled />;
const statusMessage = online ? `Online ${streamDurationString}` : "Offline"; const statusMessage = online ? `Online ${streamDurationString}` : "Offline";
@@ -145,10 +150,12 @@ export default function MainLayout(props) {
<Layout> <Layout>
<Header className={adminStyles.header}> <Header className={adminStyles.header}>
<Popover content={content} title="Thumbnail" trigger="hover">
<div className={adminStyles.statusIndicatorContainer}> <div className={adminStyles.statusIndicatorContainer}>
<span className={adminStyles.statusLabel}>{statusMessage}</span> <span className={adminStyles.statusLabel}>{statusMessage}</span>
<span className={adminStyles.statusIcon}>{statusIcon}</span> <span className={adminStyles.statusIcon}>{statusIcon}</span>
</div> </div>
</Popover>
</Header> </Header>
<Content className={adminStyles.contentMain}>{children}</Content> <Content className={adminStyles.contentMain}>{children}</Content>

View File

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

View File

@@ -1,8 +1,8 @@
/* eslint-disable no-array-constructor */ /* eslint-disable no-array-constructor */
import React, { useState, useEffect } from 'react'; import { BulbOutlined, LaptopOutlined, SaveOutlined } from "@ant-design/icons";
import { Row } from "antd"; import { Row } from "antd";
import {LaptopOutlined, BulbOutlined, SaveOutlined} from "@ant-design/icons" import React, { useEffect, useState } from 'react';
import { HARDWARE_STATS, fetchData, FETCH_INTERVAL } from '../utils/apis'; import { fetchData, FETCH_INTERVAL, HARDWARE_STATS } from '../utils/apis';
import Chart from './components/chart'; import Chart from './components/chart';
import StatisticItem from "./components/statistic"; import StatisticItem from "./components/statistic";
@@ -55,17 +55,17 @@ export default function HardwareInfo() {
const series = [ const series = [
{ {
name: "CPU", name: "CPU",
color: "#FF7700", color: "#B63FFF",
data: hardwareStatus.cpu, data: hardwareStatus.cpu,
}, },
{ {
name: "Memory", name: "Memory",
color: "#004777", color: "#2087E2",
data: hardwareStatus.memory, data: hardwareStatus.memory,
}, },
{ {
name: "Disk", name: "Disk",
color: "#A9E190", color: "#FF7700",
data: hardwareStatus.disk, data: hardwareStatus.disk,
}, },
]; ];
@@ -76,19 +76,28 @@ const series = [
<h2>Hardware Info</h2> <h2>Hardware Info</h2>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<StatisticItem <StatisticItem
title="CPU used" title={series[0].name}
value={`${currentCPUUsage} %`} value={`${currentCPUUsage}`}
prefix={<LaptopOutlined />} prefix={<LaptopOutlined style={{color: series[0].color }}/>}
color={series[0].color}
progress
centered
/> />
<StatisticItem <StatisticItem
title="Memory used" title={series[1].name}
value={`${currentRamUsage} %`} value={`${currentRamUsage}`}
prefix={<BulbOutlined />} prefix={<BulbOutlined style={{color: series[1].color }} />}
color={series[1].color}
progress
centered
/> />
<StatisticItem <StatisticItem
title="Disk used" title={series[2].name}
value={`${currentDiskUsage} %`} value={`${currentDiskUsage}`}
prefix={<SaveOutlined />} prefix={<SaveOutlined style={{color: series[2].color }} />}
color={series[2].color}
progress
centered
/> />
</Row> </Row>
@@ -96,18 +105,6 @@ const series = [
<Chart dataCollections={series} color="#FF7700" unit="%" /> <Chart dataCollections={series} color="#FF7700" unit="%" />
</div> </div>
</div> </div>
<p>cpu:[], disk: [], memory: []; value = %age.</p>
<p>the times should be the same for each, though milliseconds differ</p>
<div
style={{
border: "1px solid blue",
height: "300px",
width: "100%",
overflow: "auto",
}}
>
{JSON.stringify(hardwareStatus)}
</div>
</div> </div>
); );
} }

View File

@@ -8,7 +8,11 @@ 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, Skeleton, Typography } 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";
@@ -27,6 +31,10 @@ import { formatIPAddress, isEmptyObject } from "../utils/format";
const { Title } = Typography; const { Title } = Typography;
<<<<<<< HEAD
=======
>>>>>>> 4cdf5b73baa0584a0e6b2f586c27ca53923c65c7
<<<<<<< HEAD <<<<<<< HEAD
export default function Home() { export default function Home() {
@@ -105,6 +113,9 @@ export default function Stats() {
); );
} }
const logTable = logs.length > 0 ? <LogTable logs={logs} pageSize={5} /> : null
console.log(logs)
if (!broadcaster) { if (!broadcaster) {
return <Offline />; return <Offline />;
} }
@@ -122,17 +133,18 @@ export default function Stats() {
title="Outbound Video Stream" title="Outbound Video Stream"
value={`${setting.videoBitrate} kbps ${setting.framerate} fps`} value={`${setting.videoBitrate} kbps ${setting.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> </Row>
); );
}); });
const logTable = logs.length > 0 ? <LogTable logs={logs} pageSize={5} /> : null
const { viewerCount, sessionMaxViewerCount } = stats; const { viewerCount, sessionMaxViewerCount } = stats;
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} kpbs`;
@@ -148,16 +160,19 @@ export default function Stats() {
)}`} )}`}
value={formatDistanceToNow(new Date(broadcaster.time))} value={formatDistanceToNow(new Date(broadcaster.time))}
prefix={<ClockCircleOutlined />} prefix={<ClockCircleOutlined />}
color="#334"
/> />
<StatisticItem <StatisticItem
title="Viewers" title="Viewers"
value={viewerCount} value={viewerCount}
prefix={<UserOutlined />} prefix={<UserOutlined />}
color="#334"
/> />
<StatisticItem <StatisticItem
title="Peak viewer count" title="Peak viewer count"
value={sessionMaxViewerCount} value={sessionMaxViewerCount}
prefix={<UserOutlined />} prefix={<UserOutlined />}
color="#334"
/> />
</Row> </Row>
@@ -166,16 +181,19 @@ export default function Stats() {
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> </Row>
@@ -186,15 +204,81 @@ export default function Stats() {
title="Stream key" title="Stream key"
value={config.streamKey} value={config.streamKey}
prefix={null} prefix={null}
color="#334"
/> />
<StatisticItem <StatisticItem
title="Directory registration enabled" title="Directory registration enabled"
value={config.yp.enabled.toString()} value={config.yp.enabled.toString()}
prefix={null} prefix={null}
color="#334"
/> />
</Row> </Row>
{logTable} {logTable}
</div> </div>
); );
function Offline() {
const data = [
{
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>
);
}
} }

View File

@@ -7,7 +7,7 @@ export default function Offline() {
title: "Send some test content", title: "Send some test content",
content: ( content: (
<div> <div>
With any video you have around you can 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> <blockquote>
<em>./test/ocTestStream.sh yourVideo.mp4</em> <em>./test/ocTestStream.sh yourVideo.mp4</em>
</blockquote> </blockquote>
@@ -23,8 +23,17 @@ export default function Offline() {
) )
}, },
{ {
title: "Something else", 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 ( return (
<div> <div>
@@ -39,9 +48,9 @@ export default function Offline() {
gutter: 16, gutter: 16,
xs: 1, xs: 1,
sm: 2, sm: 2,
md: 4, md: 2,
lg: 4, lg: 6,
xl: 6, xl: 3,
xxl: 3, xxl: 3,
}} }}
dataSource={data} dataSource={data}
@@ -51,6 +60,7 @@ export default function Offline() {
</List.Item> </List.Item>
)} )}
/> />
{logTable}
</div> </div>
); );
} }

View File

@@ -12,7 +12,7 @@ function Storage({ config }) {
return ( return (
<h3> <h3>
Local storage is being used. Enable external S3 storage if you want Local storage is being used. Enable external S3 storage if you want
to use it. to use it. TODO: Make this message somewhat more informative. Point to documentation or something.
</h3> </h3>
); );
} }
@@ -74,20 +74,7 @@ export default function ServerConfig() {
return ( return (
<div> <div>
<h2>Server Config</h2>
<p>
Display this data all pretty, most things will be editable in the
future, not now.
</p>
<div
style={{
border: "1px solid pink",
width: "100%",
overflow: "auto",
}}
>
<Storage config={config} /> <Storage config={config} />
</div>
</div> </div>
); );
} }

View File

@@ -149,24 +149,9 @@ export default function ServerConfig() {
return ( return (
<div> <div>
<h2>Server Config</h2>
<p>
Display this data all pretty, most things will be editable in the
future, not now.
</p>
<div
style={{
border: "1px solid pink",
width: "100%",
overflow: "auto",
}}
>
<InstanceDetails config={config} /> <InstanceDetails config={config} />
<SocialHandles config={config} /> <SocialHandles config={config} />
<PageContent config={config} /> <PageContent config={config} />
{JSON.stringify(config)}
</div>
</div> </div>
); );
} }

View File

@@ -38,7 +38,7 @@ export default function Logs() {
<a href={release.html_url}>{release.name}</a> <a href={release.html_url}>{release.name}</a>
</Title> </Title>
<Title level={5}>{new Date(release.created_at).toDateString()}</Title> <Title level={5}>{new Date(release.created_at).toDateString()}</Title>
<ReactMarkdown>{release.body}</ReactMarkdown>;<h3>Downloads</h3> <ReactMarkdown>{release.body}</ReactMarkdown><h3>Downloads</h3>
<AssetTable {...release.assets} /> <AssetTable {...release.assets} />
</div> </div>
); );
@@ -68,6 +68,6 @@ function AssetTable(assets) {
}, },
]; ];
return <Table dataSource={data} columns={columns} rowKey="id" size="large" />; return <Table dataSource={data} columns={columns} rowKey="id" size="large" pagination={false} />
} }

View File

@@ -109,20 +109,7 @@ export default function VideoConfig() {
return ( return (
<div> <div>
<h2>Server Config</h2>
<p>
Display this data all pretty, most things will be editable in the
future, not now.
</p>
<div
style={{
border: "1px solid pink",
width: "100%",
overflow: "auto",
}}
>
<VideoVariants config={config} /> <VideoVariants config={config} />
</div>
</div> </div>
); );
} }

View File

@@ -109,22 +109,25 @@ export default function ViewersOverTime() {
title="Current viewers" title="Current viewers"
value={viewerCount.toString()} value={viewerCount.toString()}
prefix={<UserOutlined />} prefix={<UserOutlined />}
color="#334"
/> />
<StatisticItem <StatisticItem
title="Peak viewers this session" title="Peak viewers this session"
value={sessionPeakViewerCount.toString()} value={sessionPeakViewerCount.toString()}
prefix={<UserOutlined />} prefix={<UserOutlined />}
color="#334"
/> />
<StatisticItem <StatisticItem
title="Peak viewers overall" title="Peak viewers overall"
value={overallPeakViewerCount.toString()} value={overallPeakViewerCount.toString()}
prefix={<UserOutlined />} prefix={<UserOutlined />}
color="#334"
/> />
</Row> </Row>
<div className="chart-container"> <div className="chart-container">
<Chart data={viewerInfo} color="#ff84d8" unit="" /> <Chart title="Viewers" data={viewerInfo} color="#2087E2" unit="" />
</div> </div>
<Table dataSource={clients} columns={columns} />; <Table dataSource={clients} columns={columns} />
</div> </div>
); );
} }

View File

@@ -37,16 +37,19 @@ export const LOGS_WARN = `${API_LOCATION}logs/warnings`;
const GITHUB_RELEASE_URL = "https://api.github.com/repos/owncast/owncast/releases/latest"; const GITHUB_RELEASE_URL = "https://api.github.com/repos/owncast/owncast/releases/latest";
export async function fetchData(url) { export async function fetchData(url) {
const encoded = btoa(`${ADMIN_USERNAME}:${ADMIN_STREAMKEY}`); let options: RequestInit = {};
if (ADMIN_USERNAME && ADMIN_STREAMKEY) {
const encoded = btoa(`${ADMIN_USERNAME}:${ADMIN_STREAMKEY}`);
options.headers = {
'Authorization': `Basic ${encoded}`
}
options.mode = 'cors';
options.credentials = 'include'
}
try { try {
const response = await fetch(url, { const response = await fetch(url, options);
headers: {
'Authorization': `Basic ${encoded}`,
},
mode: 'cors',
credentials: 'include',
});
if (!response.ok) { if (!response.ok) {
const message = `An error has occured: ${response.status}`; const message = `An error has occured: ${response.status}`;
throw new Error(message); throw new Error(message);