viewer-info.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import React, { useState, useEffect, useContext, ReactElement } from 'react';
  2. import { Row, Col, Typography, Menu, Dropdown, Spin, Alert } from 'antd';
  3. import { getUnixTime, sub } from 'date-fns';
  4. import dynamic from 'next/dynamic';
  5. import { Chart } from '../../components/admin/Chart';
  6. import { StatisticItem } from '../../components/admin/StatisticItem';
  7. import { ViewerTable } from '../../components/admin/ViewerTable';
  8. import { ServerStatusContext } from '../../utils/server-status-context';
  9. import { VIEWERS_OVER_TIME, ACTIVE_VIEWER_DETAILS, fetchData } from '../../utils/apis';
  10. import { AdminLayout } from '../../components/layouts/AdminLayout';
  11. // Lazy loaded components
  12. const DownOutlined = dynamic(() => import('@ant-design/icons/DownOutlined'), {
  13. ssr: false,
  14. });
  15. const UserOutlined = dynamic(() => import('@ant-design/icons/UserOutlined'), {
  16. ssr: false,
  17. });
  18. const FETCH_INTERVAL = 60 * 1000; // 1 min
  19. export default function ViewersOverTime() {
  20. const context = useContext(ServerStatusContext);
  21. const { online, broadcaster, viewerCount, overallPeakViewerCount, sessionPeakViewerCount } =
  22. context || {};
  23. let streamStart;
  24. if (broadcaster && broadcaster.time) {
  25. streamStart = new Date(broadcaster.time);
  26. }
  27. const times = [
  28. { title: 'Current stream', start: streamStart },
  29. { title: 'Last 12 hours', start: sub(new Date(), { hours: 12 }) },
  30. { title: 'Last 24 hours', start: sub(new Date(), { hours: 24 }) },
  31. { title: 'Last 7 days', start: sub(new Date(), { days: 7 }) },
  32. { title: 'Last 30 days', start: sub(new Date(), { days: 30 }) },
  33. { title: 'Last 3 months', start: sub(new Date(), { months: 3 }) },
  34. { title: 'Last 6 months', start: sub(new Date(), { months: 6 }) },
  35. ];
  36. const [loadingChart, setLoadingChart] = useState(true);
  37. const [viewerInfo, setViewerInfo] = useState([]);
  38. const [viewerDetails, setViewerDetails] = useState([]);
  39. const [timeWindowStart, setTimeWindowStart] = useState(times[1]);
  40. const getInfo = async () => {
  41. try {
  42. const url = `${VIEWERS_OVER_TIME}?windowStart=${getUnixTime(timeWindowStart.start)}`;
  43. const result = await fetchData(url);
  44. setViewerInfo(result);
  45. setLoadingChart(false);
  46. } catch (error) {
  47. console.log('==== error', error);
  48. }
  49. try {
  50. const result = await fetchData(ACTIVE_VIEWER_DETAILS);
  51. setViewerDetails(result);
  52. } catch (error) {
  53. console.log('==== error', error);
  54. }
  55. };
  56. useEffect(() => {
  57. let getStatusIntervalId = null;
  58. getInfo();
  59. if (online) {
  60. getStatusIntervalId = setInterval(getInfo, FETCH_INTERVAL);
  61. // returned function will be called on component unmount
  62. return () => {
  63. clearInterval(getStatusIntervalId);
  64. };
  65. }
  66. return () => [];
  67. }, [online, timeWindowStart]);
  68. const onTimeWindowSelect = ({ key }) => {
  69. setTimeWindowStart(times[key]);
  70. };
  71. const menu = (
  72. <Menu>
  73. {online && streamStart && (
  74. <Menu.Item key="0" onClick={onTimeWindowSelect}>
  75. {times[0].title}
  76. </Menu.Item>
  77. )}
  78. {times.slice(1).map((time, index) => (
  79. // The array is hard coded, so it's safe to use the index as a key.
  80. // eslint-disable-next-line react/no-array-index-key
  81. <Menu.Item key={index + 1} onClick={onTimeWindowSelect}>
  82. {time.title}
  83. </Menu.Item>
  84. ))}
  85. </Menu>
  86. );
  87. return (
  88. <>
  89. <Typography.Title>Viewer Info</Typography.Title>
  90. <br />
  91. <Row gutter={[16, 16]} justify="space-around">
  92. {online && (
  93. <Col span={8} md={8}>
  94. <StatisticItem
  95. title="Current viewers"
  96. value={viewerCount.toString()}
  97. prefix={<UserOutlined />}
  98. />
  99. </Col>
  100. )}
  101. <Col md={online ? 8 : 12}>
  102. <StatisticItem
  103. title={online ? 'Max viewers this stream' : 'Max viewers last stream'}
  104. value={sessionPeakViewerCount.toString()}
  105. prefix={<UserOutlined />}
  106. />
  107. </Col>
  108. <Col md={online ? 8 : 12}>
  109. <StatisticItem
  110. title="All-time max viewers"
  111. value={overallPeakViewerCount.toString()}
  112. prefix={<UserOutlined />}
  113. />
  114. </Col>
  115. </Row>
  116. {!viewerInfo.length && (
  117. <Alert
  118. style={{ marginTop: '10px' }}
  119. banner
  120. message="Please wait"
  121. description="No viewer data has been collected yet."
  122. type="info"
  123. />
  124. )}
  125. <Spin spinning={!viewerInfo.length || loadingChart}>
  126. <Dropdown overlay={menu} trigger={['click']}>
  127. <button
  128. type="button"
  129. style={{ float: 'right', background: 'transparent', border: 'unset' }}
  130. >
  131. {timeWindowStart.title} <DownOutlined />
  132. </button>
  133. </Dropdown>
  134. {viewerInfo.length > 0 && (
  135. <Chart
  136. title="Viewers"
  137. data={viewerInfo}
  138. color="#2087E2"
  139. unit="viewers"
  140. minYValue={0}
  141. yStepSize={1}
  142. />
  143. )}
  144. <ViewerTable data={viewerDetails} />
  145. </Spin>
  146. </>
  147. );
  148. }
  149. ViewersOverTime.getLayout = function getLayout(page: ReactElement) {
  150. return <AdminLayout page={page} />;
  151. };