diff --git a/web/package-lock.json b/web/package-lock.json index fc7a06f7f..65e32d894 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -5322,6 +5322,14 @@ } } }, + "linkify-it": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", + "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "requires": { + "uc.micro": "^1.0.1" + } + }, "load-json-file": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", @@ -7147,6 +7155,15 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-linkify": { + "version": "1.0.0-alpha", + "resolved": "https://registry.npmjs.org/react-linkify/-/react-linkify-1.0.0-alpha.tgz", + "integrity": "sha512-7gcIUvJkAXXttt1fmBK9cwn+1jTa4hbKLGCZ9J1U6EOkyb2/+LKL1Z28d9rtDLMnpvImlNlLPdTPooorl5cpmg==", + "requires": { + "linkify-it": "^2.0.3", + "tlds": "^1.199.0" + } + }, "react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -8726,6 +8743,11 @@ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==" }, + "tlds": { + "version": "1.212.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.212.0.tgz", + "integrity": "sha512-03rYYO1rGhOYpdYB+wlLY2d0xza6hdN/S67ol2ZpaH+CtFedMVAVhj8ft0rwxEkr90zatou8opBv7Xp6X4cK6g==" + }, "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", @@ -8878,6 +8900,11 @@ "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==", "dev": true }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", diff --git a/web/package.json b/web/package.json index 55bfd1ee6..9135e3df3 100644 --- a/web/package.json +++ b/web/package.json @@ -18,6 +18,7 @@ "prop-types": "^15.7.2", "react": "16.13.1", "react-dom": "16.13.1", + "react-linkify": "^1.0.0-alpha", "recharts": "^1.8.5", "sass": "^1.26.11" }, diff --git a/web/pages/components/chart.tsx b/web/pages/components/chart.tsx index e1680bf78..6f4c1ce39 100644 --- a/web/pages/components/chart.tsx +++ b/web/pages/components/chart.tsx @@ -9,13 +9,14 @@ interface ToolTipProps { const defaultProps = { active: false, payload: {}, + unit: "" }; interface ChartProps { - data: number, + data: [{}], color: string, unit: string, - dataCollections?: [], + dataCollections?: {}, } function CustomizedTooltip(props: ToolTipProps) { @@ -40,7 +41,7 @@ export default function Chart({ data, color, unit, dataCollections }: ChartProps }; if (dataCollections) { - var ticks = dataCollections[0]?.data.map(function (collection) { + var ticks = dataCollections?[0].data.map(function (collection) { return collection?.time; }) } else { diff --git a/web/pages/components/log-table.tsx b/web/pages/components/log-table.tsx new file mode 100644 index 000000000..5844a8a59 --- /dev/null +++ b/web/pages/components/log-table.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { timeFormat } from "d3-time-format"; +import { Table, } from "antd"; +import Linkify from "react-linkify"; + +export default function LogTable({ logs, pageSize }) { + const columns = [ + { + title: "Level", + dataIndex: "level", + key: "level", + filters: [ + { + text: "Info", + value: "info", + }, + { + text: "Warning", + value: "warning", + }, + { + text: "Error", + value: "Error", + }, + ], + onFilter: (level, row) => row.level.indexOf(level) === 0, + render: renderColumnLevel, + }, + { + title: "Timestamp", + dataIndex: "time", + key: "time", + render: (timestamp) => + timeFormat("%H:%M:%S %m/%d/%Y")(new Date(timestamp)), + sorter: (a, b) => new Date(a.time).getTime() - new Date(b.time).getTime(), + sortDirections: ["descend", "ascend"], + defaultSortOrder: "descend", + }, + { + title: "Message", + dataIndex: "message", + key: "message", + render: renderMessage, + }, + ]; + + return ( +
+ row.time} + pagination={{ pageSize: pageSize || 20 }} + /> + ; + + ); +} + +function renderColumnLevel(text, entry) { + let color = 'black'; + + if (entry.level === "warning") { + color = "orange"; + } else if (entry.level === 'error') { + color = "red"; + } + + const style = { + color, + }; + return
{text}
; +} + +function renderMessage(text, entry) { + return ( + {text} + ) +} \ No newline at end of file diff --git a/web/pages/components/main-layout.tsx b/web/pages/components/main-layout.tsx index 9420e2971..0d96e8ef1 100644 --- a/web/pages/components/main-layout.tsx +++ b/web/pages/components/main-layout.tsx @@ -98,8 +98,8 @@ export default function MainLayout(props) { Storage - - Change Stream Key + + Logs diff --git a/web/pages/index.tsx b/web/pages/index.tsx index e17a5dbf0..e3fbd5141 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -13,9 +13,12 @@ import { UserOutlined, ClockCircleOutlined } from "@ant-design/icons"; import { formatDistanceToNow, formatRelative } from "date-fns"; import { BroadcastStatusContext } from "./utils/broadcast-status-context"; import StatisticItem from "./components/statistic" +import LogTable from "./components/log-table"; + import { STREAM_STATUS, SERVER_CONFIG, + LOGS_WARN, fetchData, FETCH_INTERVAL, } from "./utils/apis"; @@ -50,11 +53,16 @@ export default function Stats() { } catch (error) { console.log(error); } + + getConfig(); + getLogs(); }; // Pull in the server config so we can show config overview. const [config, setConfig] = useState([]); + const [logs, setLogs] = useState([]); + const getConfig = async () => { try { const result = await fetchData(SERVER_CONFIG); @@ -64,10 +72,18 @@ export default function Stats() { } }; + const getLogs = async () => { + try { + const result = await fetchData(LOGS_WARN); + setLogs(result); + } catch (error) { + console.log("==== error", error); + } + }; + useEffect(() => { setInterval(getStats, FETCH_INTERVAL); getStats(); - getConfig(); }, []); if (isEmptyObject(config) || isEmptyObject(stats)) { @@ -107,8 +123,9 @@ export default function Stats() { ); }); + const logTable = logs.length > 0 ? : null const { viewerCount, sessionMaxViewerCount, lastConnectTime } = stats; - const streamVideoDetailString = `${streamDetails.width}x${streamDetails.height} ${streamDetails.videoBitrate} kbps ${streamDetails.framerate} fps `; + const streamVideoDetailString = `${streamDetails.videoCodec} ${streamDetails.videoBitrate} kbps ${streamDetails.width}x${streamDetails.height}`; const streamAudioDetailString = `${streamDetails.audioCodec} ${streamDetails.audioBitrate} kpbs`; return ( @@ -167,6 +184,8 @@ export default function Stats() { prefix={null} /> + + {logTable} ); } diff --git a/web/pages/logs.tsx b/web/pages/logs.tsx new file mode 100644 index 000000000..e54438ba9 --- /dev/null +++ b/web/pages/logs.tsx @@ -0,0 +1,38 @@ +import React, { useState, useEffect } from "react"; +import LogTable from "./components/log-table" + +import { + LOGS_ALL, + fetchData, +} from "./utils/apis"; + +const FETCH_INTERVAL = 5 * 1000; // 5 sec + +export default function Logs() { + const [logs, setLogs] = useState([]); + + const getInfo = async () => { + try { + const result = await fetchData(LOGS_ALL); + setLogs(result); + } catch (error) { + console.log("==== error", error); + } + }; + + useEffect(() => { + let getStatusIntervalId = null; + + setInterval(getInfo, FETCH_INTERVAL); + getInfo(); + + getStatusIntervalId = setInterval(getInfo, FETCH_INTERVAL); + // returned function will be called on component unmount + return () => { + clearInterval(getStatusIntervalId); + }; + }, []); + + return ; +} + diff --git a/web/pages/utils/apis.ts b/web/pages/utils/apis.ts index a6b1eade1..b0db43f85 100644 --- a/web/pages/utils/apis.ts +++ b/web/pages/utils/apis.ts @@ -25,10 +25,15 @@ export const VIEWERS_OVER_TIME = `${API_LOCATION}viewersOverTime`; // Get currently connected clients export const CONNECTED_CLIENTS = `${API_LOCATION}clients`; - // Get hardware stats export const HARDWARE_STATS = `${API_LOCATION}hardwarestats`; +// Get all logs +export const LOGS_ALL = `${API_LOCATION}logs`; + +// Get warnings + errors +export const LOGS_WARN = `${API_LOCATION}logs/warnings`; + // Current Stream status. // This is literally the same as /api/status except it supports // auth.