29 changed files with 323 additions and 281 deletions
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
import { AppProps } from 'next/app'; |
||||
|
||||
function SimpleLayout({ Component, pageProps }: AppProps) { |
||||
return ( |
||||
<div> |
||||
<Component {...pageProps} /> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
export default SimpleLayout; |
||||
@ -1,6 +1,5 @@
@@ -1,6 +1,5 @@
|
||||
const withLess = require('next-with-less'); |
||||
|
||||
module.exports = withLess({ |
||||
basePath: '/admin', |
||||
trailingSlash: true, |
||||
}); |
||||
|
||||
@ -1,15 +1,15 @@
@@ -1,15 +1,15 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons'; |
||||
import { Button, Checkbox, Input, Modal, Space, Table, Typography } from 'antd'; |
||||
import React, { useContext, useEffect, useState } from 'react'; |
||||
import FormStatusIndicator from '../components/config/form-status-indicator'; |
||||
import FormStatusIndicator from '../../components/config/form-status-indicator'; |
||||
import { |
||||
API_EXTERNAL_ACTIONS, |
||||
postConfigUpdateToAPI, |
||||
RESET_TIMEOUT, |
||||
} from '../utils/config-constants'; |
||||
import { createInputStatus, STATUS_ERROR, STATUS_SUCCESS } from '../utils/input-statuses'; |
||||
import { ServerStatusContext } from '../utils/server-status-context'; |
||||
import isValidUrl, { DEFAULT_TEXTFIELD_URL_PATTERN } from '../utils/urls'; |
||||
} from '../../utils/config-constants'; |
||||
import { createInputStatus, STATUS_ERROR, STATUS_SUCCESS } from '../../utils/input-statuses'; |
||||
import { ServerStatusContext } from '../../utils/server-status-context'; |
||||
import isValidUrl, { DEFAULT_TEXTFIELD_URL_PATTERN } from '../../utils/urls'; |
||||
|
||||
const { Title, Paragraph } = Typography; |
||||
let resetTimer = null; |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
import { AppProps } from 'next/app'; |
||||
import ServerStatusProvider from '../../utils/server-status-context'; |
||||
import AlertMessageProvider from '../../utils/alert-message-context'; |
||||
import MainLayout from '../../components/main-layout'; |
||||
|
||||
function AdminLayout({ Component, pageProps }: AppProps) { |
||||
return ( |
||||
<ServerStatusProvider> |
||||
<AlertMessageProvider> |
||||
<MainLayout> |
||||
<Component {...pageProps} /> |
||||
</MainLayout> |
||||
</AlertMessageProvider> |
||||
</ServerStatusProvider> |
||||
); |
||||
} |
||||
|
||||
export default AdminLayout; |
||||
@ -1,16 +1,16 @@
@@ -1,16 +1,16 @@
|
||||
import React, { useState, useEffect, useContext } from 'react'; |
||||
import { Tabs } from 'antd'; |
||||
import { ServerStatusContext } from '../../utils/server-status-context'; |
||||
import { ServerStatusContext } from '../../../utils/server-status-context'; |
||||
import { |
||||
CONNECTED_CLIENTS, |
||||
fetchData, |
||||
DISABLED_USERS, |
||||
MODERATORS, |
||||
BANNED_IPS, |
||||
} from '../../utils/apis'; |
||||
import UserTable from '../../components/user-table'; |
||||
import ClientTable from '../../components/client-table'; |
||||
import BannedIPsTable from '../../components/banned-ips-table'; |
||||
} from '../../../utils/apis'; |
||||
import UserTable from '../../../components/user-table'; |
||||
import ClientTable from '../../../components/client-table'; |
||||
import BannedIPsTable from '../../../components/banned-ips-table'; |
||||
|
||||
const { TabPane } = Tabs; |
||||
|
||||
@ -1,11 +1,11 @@
@@ -1,11 +1,11 @@
|
||||
import React from 'react'; |
||||
import { Typography } from 'antd'; |
||||
|
||||
import EditInstanceDetails from '../components/config/edit-instance-details'; |
||||
import EditInstanceTags from '../components/config/edit-tags'; |
||||
import EditSocialLinks from '../components/config/edit-social-links'; |
||||
import EditPageContent from '../components/config/edit-page-content'; |
||||
import EditCustomStyles from '../components/config/edit-custom-css'; |
||||
import EditInstanceDetails from '../../components/config/edit-instance-details'; |
||||
import EditInstanceTags from '../../components/config/edit-tags'; |
||||
import EditSocialLinks from '../../components/config/edit-social-links'; |
||||
import EditPageContent from '../../components/config/edit-page-content'; |
||||
import EditCustomStyles from '../../components/config/edit-custom-css'; |
||||
|
||||
const { Title } = Typography; |
||||
|
||||
@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
|
||||
import React from 'react'; |
||||
import { Typography } from 'antd'; |
||||
import EditServerDetails from '../components/config/edit-server-details'; |
||||
import EditServerDetails from '../../components/config/edit-server-details'; |
||||
|
||||
const { Title } = Typography; |
||||
|
||||
@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
|
||||
import React from 'react'; |
||||
import { Typography } from 'antd'; |
||||
import EditSocialLinks from '../components/config/edit-social-links'; |
||||
import EditSocialLinks from '../../components/config/edit-social-links'; |
||||
|
||||
const { Title } = Typography; |
||||
|
||||
@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
|
||||
import { Typography } from 'antd'; |
||||
import React from 'react'; |
||||
import EditStorage from '../components/config/edit-storage'; |
||||
import EditStorage from '../../components/config/edit-storage'; |
||||
|
||||
const { Title } = Typography; |
||||
|
||||
@ -1,8 +1,8 @@
@@ -1,8 +1,8 @@
|
||||
import { Col, Collapse, Row, Typography } from 'antd'; |
||||
import React from 'react'; |
||||
import VideoCodecSelector from '../components/config/video-codec-selector'; |
||||
import VideoLatency from '../components/config/video-latency'; |
||||
import VideoVariantsTable from '../components/config/video-variants-table'; |
||||
import VideoCodecSelector from '../../components/config/video-codec-selector'; |
||||
import VideoLatency from '../../components/config/video-latency'; |
||||
import VideoVariantsTable from '../../components/config/video-variants-table'; |
||||
|
||||
const { Panel } = Collapse; |
||||
const { Title } = Typography; |
||||
@ -1,9 +1,9 @@
@@ -1,9 +1,9 @@
|
||||
import { BulbOutlined, LaptopOutlined, SaveOutlined } from '@ant-design/icons'; |
||||
import { Row, Col, Typography } from 'antd'; |
||||
import React, { useEffect, useState } from 'react'; |
||||
import { fetchData, FETCH_INTERVAL, HARDWARE_STATS } from '../utils/apis'; |
||||
import Chart from '../components/chart'; |
||||
import StatisticItem from '../components/statistic'; |
||||
import { fetchData, FETCH_INTERVAL, HARDWARE_STATS } from '../../utils/apis'; |
||||
import Chart from '../../components/chart'; |
||||
import StatisticItem from '../../components/statistic'; |
||||
|
||||
// TODO: FIX TS WARNING FROM THIS.
|
||||
// interface TimedValue {
|
||||
@ -0,0 +1,180 @@
@@ -0,0 +1,180 @@
|
||||
import React, { useState, useEffect, useContext } from 'react'; |
||||
import { Skeleton, Card, Statistic, Row, Col } from 'antd'; |
||||
import { UserOutlined, ClockCircleOutlined } from '@ant-design/icons'; |
||||
import { formatDistanceToNow, formatRelative } from 'date-fns'; |
||||
import { ServerStatusContext } from '../../utils/server-status-context'; |
||||
import LogTable from '../../components/log-table'; |
||||
import Offline from '../../components/offline-notice'; |
||||
import StreamHealthOverview from '../../components/stream-health-overview'; |
||||
|
||||
import { LOGS_WARN, fetchData, FETCH_INTERVAL } from '../../utils/apis'; |
||||
import { formatIPAddress, isEmptyObject } from '../../utils/format'; |
||||
import NewsFeed from '../../components/news-feed'; |
||||
|
||||
function streamDetailsFormatter(streamDetails) { |
||||
return ( |
||||
<ul className="statistics-list"> |
||||
<li> |
||||
{streamDetails.videoCodec || 'Unknown'} @ {streamDetails.videoBitrate || 'Unknown'} kbps |
||||
</li> |
||||
<li>{streamDetails.framerate || 'Unknown'} fps</li> |
||||
<li> |
||||
{streamDetails.width} x {streamDetails.height} |
||||
</li> |
||||
</ul> |
||||
); |
||||
} |
||||
|
||||
export default function Home() { |
||||
const serverStatusData = useContext(ServerStatusContext); |
||||
const { broadcaster, serverConfig: configData } = serverStatusData || {}; |
||||
const { remoteAddr, streamDetails } = broadcaster || {}; |
||||
|
||||
const encoder = streamDetails?.encoder || 'Unknown encoder'; |
||||
|
||||
const [logsData, setLogs] = useState([]); |
||||
const getLogs = async () => { |
||||
try { |
||||
const result = await fetchData(LOGS_WARN); |
||||
setLogs(result); |
||||
} catch (error) { |
||||
console.log('==== error', error); |
||||
} |
||||
}; |
||||
const getMoreStats = () => { |
||||
getLogs(); |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
getMoreStats(); |
||||
|
||||
let intervalId = null; |
||||
intervalId = setInterval(getMoreStats, FETCH_INTERVAL); |
||||
|
||||
return () => { |
||||
clearInterval(intervalId); |
||||
}; |
||||
}, []); |
||||
|
||||
if (isEmptyObject(configData) || isEmptyObject(serverStatusData)) { |
||||
return ( |
||||
<> |
||||
<Skeleton active /> |
||||
<Skeleton active /> |
||||
<Skeleton active /> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
if (!broadcaster) { |
||||
return <Offline logs={logsData} config={configData} />; |
||||
} |
||||
|
||||
// map out settings
|
||||
const videoQualitySettings = serverStatusData?.currentBroadcast?.outputSettings?.map(setting => { |
||||
const { audioPassthrough, videoPassthrough, audioBitrate, videoBitrate, framerate } = setting; |
||||
|
||||
const audioSetting = audioPassthrough |
||||
? `${streamDetails.audioCodec || 'Unknown'}, ${streamDetails.audioBitrate} kbps` |
||||
: `${audioBitrate || 'Unknown'} kbps`; |
||||
|
||||
const videoSetting = videoPassthrough |
||||
? `${streamDetails.videoBitrate || 'Unknown'} kbps, ${streamDetails.framerate} fps ${ |
||||
streamDetails.width |
||||
} x ${streamDetails.height}` |
||||
: `${videoBitrate || 'Unknown'} kbps, ${framerate} fps`; |
||||
|
||||
return ( |
||||
<div className="stream-details-item-container"> |
||||
<Statistic |
||||
className="stream-details-item" |
||||
title="Outbound Video Stream" |
||||
value={videoSetting} |
||||
/> |
||||
<Statistic |
||||
className="stream-details-item" |
||||
title="Outbound Audio Stream" |
||||
value={audioSetting} |
||||
/> |
||||
</div> |
||||
); |
||||
}); |
||||
|
||||
// inbound
|
||||
const { viewerCount, sessionPeakViewerCount } = serverStatusData; |
||||
|
||||
const streamAudioDetailString = `${streamDetails.audioCodec}, ${ |
||||
streamDetails.audioBitrate || 'Unknown' |
||||
} kbps`;
|
||||
|
||||
const broadcastDate = new Date(broadcaster.time); |
||||
|
||||
return ( |
||||
<div className="home-container"> |
||||
<div className="sections-container"> |
||||
<div className="online-status-section"> |
||||
<Card size="small" type="inner" className="online-details-card"> |
||||
<Row gutter={[16, 16]} align="middle"> |
||||
<Col span={8} sm={24} md={8}> |
||||
<Statistic |
||||
title={`Stream started ${formatRelative(broadcastDate, Date.now())}`} |
||||
value={formatDistanceToNow(broadcastDate)} |
||||
prefix={<ClockCircleOutlined />} |
||||
/> |
||||
</Col> |
||||
<Col span={8} sm={24} md={8}> |
||||
<Statistic title="Viewers" value={viewerCount} prefix={<UserOutlined />} /> |
||||
</Col> |
||||
<Col span={8} sm={24} md={8}> |
||||
<Statistic |
||||
title="Peak viewer count" |
||||
value={sessionPeakViewerCount} |
||||
prefix={<UserOutlined />} |
||||
/> |
||||
</Col> |
||||
</Row> |
||||
<StreamHealthOverview /> |
||||
</Card> |
||||
</div> |
||||
|
||||
<Row gutter={[16, 16]} className="section stream-details-section"> |
||||
<Col className="stream-details" span={12} sm={24} md={24} lg={12}> |
||||
<Card |
||||
size="small" |
||||
title="Outbound Stream Details" |
||||
type="inner" |
||||
className="outbound-details" |
||||
> |
||||
{videoQualitySettings} |
||||
</Card> |
||||
|
||||
<Card size="small" title="Inbound Stream Details" type="inner"> |
||||
<Statistic |
||||
className="stream-details-item" |
||||
title="Input" |
||||
value={`${encoder} ${formatIPAddress(remoteAddr)}`} |
||||
/> |
||||
<Statistic |
||||
className="stream-details-item" |
||||
title="Inbound Video Stream" |
||||
value={streamDetails} |
||||
formatter={streamDetailsFormatter} |
||||
/> |
||||
<Statistic |
||||
className="stream-details-item" |
||||
title="Inbound Audio Stream" |
||||
value={streamAudioDetailString} |
||||
/> |
||||
</Card> |
||||
</Col> |
||||
|
||||
<Col span={12} xs={24} sm={24} md={24} lg={12}> |
||||
<NewsFeed /> |
||||
</Col> |
||||
</Row> |
||||
</div> |
||||
<br /> |
||||
<LogTable logs={logsData} pageSize={5} /> |
||||
</div> |
||||
); |
||||
} |
||||
@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react'; |
||||
import LogTable from '../components/log-table'; |
||||
import LogTable from '../../components/log-table'; |
||||
|
||||
import { LOGS_ALL, fetchData } from '../utils/apis'; |
||||
import { LOGS_ALL, fetchData } from '../../utils/apis'; |
||||
|
||||
const FETCH_INTERVAL = 5 * 1000; // 5 sec
|
||||
|
||||
@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react'; |
||||
import ReactMarkdown from 'react-markdown'; |
||||
import { Table, Typography } from 'antd'; |
||||
import { getGithubRelease } from '../utils/apis'; |
||||
import { getGithubRelease } from '../../utils/apis'; |
||||
|
||||
const { Title } = Typography; |
||||
|
||||
@ -1,180 +1,8 @@
@@ -1,180 +1,8 @@
|
||||
import React, { useState, useEffect, useContext } from 'react'; |
||||
import { Skeleton, Card, Statistic, Row, Col } from 'antd'; |
||||
import { UserOutlined, ClockCircleOutlined } from '@ant-design/icons'; |
||||
import { formatDistanceToNow, formatRelative } from 'date-fns'; |
||||
import { ServerStatusContext } from '../utils/server-status-context'; |
||||
import LogTable from '../components/log-table'; |
||||
import Offline from '../components/offline-notice'; |
||||
import StreamHealthOverview from '../components/stream-health-overview'; |
||||
|
||||
import { LOGS_WARN, fetchData, FETCH_INTERVAL } from '../utils/apis'; |
||||
import { formatIPAddress, isEmptyObject } from '../utils/format'; |
||||
import NewsFeed from '../components/news-feed'; |
||||
|
||||
function streamDetailsFormatter(streamDetails) { |
||||
return ( |
||||
<ul className="statistics-list"> |
||||
<li> |
||||
{streamDetails.videoCodec || 'Unknown'} @ {streamDetails.videoBitrate || 'Unknown'} kbps |
||||
</li> |
||||
<li>{streamDetails.framerate || 'Unknown'} fps</li> |
||||
<li> |
||||
{streamDetails.width} x {streamDetails.height} |
||||
</li> |
||||
</ul> |
||||
); |
||||
} |
||||
|
||||
export default function Home() { |
||||
const serverStatusData = useContext(ServerStatusContext); |
||||
const { broadcaster, serverConfig: configData } = serverStatusData || {}; |
||||
const { remoteAddr, streamDetails } = broadcaster || {}; |
||||
|
||||
const encoder = streamDetails?.encoder || 'Unknown encoder'; |
||||
|
||||
const [logsData, setLogs] = useState([]); |
||||
const getLogs = async () => { |
||||
try { |
||||
const result = await fetchData(LOGS_WARN); |
||||
setLogs(result); |
||||
} catch (error) { |
||||
console.log('==== error', error); |
||||
} |
||||
}; |
||||
const getMoreStats = () => { |
||||
getLogs(); |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
getMoreStats(); |
||||
|
||||
let intervalId = null; |
||||
intervalId = setInterval(getMoreStats, FETCH_INTERVAL); |
||||
|
||||
return () => { |
||||
clearInterval(intervalId); |
||||
}; |
||||
}, []); |
||||
|
||||
if (isEmptyObject(configData) || isEmptyObject(serverStatusData)) { |
||||
return ( |
||||
<> |
||||
<Skeleton active /> |
||||
<Skeleton active /> |
||||
<Skeleton active /> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
if (!broadcaster) { |
||||
return <Offline logs={logsData} config={configData} />; |
||||
} |
||||
|
||||
// map out settings
|
||||
const videoQualitySettings = serverStatusData?.currentBroadcast?.outputSettings?.map(setting => { |
||||
const { audioPassthrough, videoPassthrough, audioBitrate, videoBitrate, framerate } = setting; |
||||
|
||||
const audioSetting = audioPassthrough |
||||
? `${streamDetails.audioCodec || 'Unknown'}, ${streamDetails.audioBitrate} kbps` |
||||
: `${audioBitrate || 'Unknown'} kbps`; |
||||
|
||||
const videoSetting = videoPassthrough |
||||
? `${streamDetails.videoBitrate || 'Unknown'} kbps, ${streamDetails.framerate} fps ${ |
||||
streamDetails.width |
||||
} x ${streamDetails.height}` |
||||
: `${videoBitrate || 'Unknown'} kbps, ${framerate} fps`; |
||||
|
||||
return ( |
||||
<div className="stream-details-item-container"> |
||||
<Statistic |
||||
className="stream-details-item" |
||||
title="Outbound Video Stream" |
||||
value={videoSetting} |
||||
/> |
||||
<Statistic |
||||
className="stream-details-item" |
||||
title="Outbound Audio Stream" |
||||
value={audioSetting} |
||||
/> |
||||
</div> |
||||
); |
||||
}); |
||||
|
||||
// inbound
|
||||
const { viewerCount, sessionPeakViewerCount } = serverStatusData; |
||||
|
||||
const streamAudioDetailString = `${streamDetails.audioCodec}, ${ |
||||
streamDetails.audioBitrate || 'Unknown' |
||||
} kbps`;
|
||||
|
||||
const broadcastDate = new Date(broadcaster.time); |
||||
|
||||
return ( |
||||
<div className="home-container"> |
||||
<div className="sections-container"> |
||||
<div className="online-status-section"> |
||||
<Card size="small" type="inner" className="online-details-card"> |
||||
<Row gutter={[16, 16]} align="middle"> |
||||
<Col span={8} sm={24} md={8}> |
||||
<Statistic |
||||
title={`Stream started ${formatRelative(broadcastDate, Date.now())}`} |
||||
value={formatDistanceToNow(broadcastDate)} |
||||
prefix={<ClockCircleOutlined />} |
||||
/> |
||||
</Col> |
||||
<Col span={8} sm={24} md={8}> |
||||
<Statistic title="Viewers" value={viewerCount} prefix={<UserOutlined />} /> |
||||
</Col> |
||||
<Col span={8} sm={24} md={8}> |
||||
<Statistic |
||||
title="Peak viewer count" |
||||
value={sessionPeakViewerCount} |
||||
prefix={<UserOutlined />} |
||||
/> |
||||
</Col> |
||||
</Row> |
||||
<StreamHealthOverview /> |
||||
</Card> |
||||
</div> |
||||
|
||||
<Row gutter={[16, 16]} className="section stream-details-section"> |
||||
<Col className="stream-details" span={12} sm={24} md={24} lg={12}> |
||||
<Card |
||||
size="small" |
||||
title="Outbound Stream Details" |
||||
type="inner" |
||||
className="outbound-details" |
||||
> |
||||
{videoQualitySettings} |
||||
</Card> |
||||
|
||||
<Card size="small" title="Inbound Stream Details" type="inner"> |
||||
<Statistic |
||||
className="stream-details-item" |
||||
title="Input" |
||||
value={`${encoder} ${formatIPAddress(remoteAddr)}`} |
||||
/> |
||||
<Statistic |
||||
className="stream-details-item" |
||||
title="Inbound Video Stream" |
||||
value={streamDetails} |
||||
formatter={streamDetailsFormatter} |
||||
/> |
||||
<Statistic |
||||
className="stream-details-item" |
||||
title="Inbound Audio Stream" |
||||
value={streamAudioDetailString} |
||||
/> |
||||
</Card> |
||||
</Col> |
||||
|
||||
<Col span={12} xs={24} sm={24} md={24} lg={12}> |
||||
<NewsFeed /> |
||||
</Col> |
||||
</Row> |
||||
</div> |
||||
<br /> |
||||
<LogTable logs={logsData} pageSize={5} /> |
||||
<div> |
||||
This is where v2 of the Owncast web UI will be built. Begin with the layout component |
||||
https://ant.design/components/layout/ and edit pages/index.tsx.
|
||||
</div> |
||||
); |
||||
} |
||||
|
||||
Loading…
Reference in new issue