mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-12-27 09:00:28 +08:00
Implement Network overview tab in React
This commit is contained in:
parent
a095eb2178
commit
6d9fcab656
@ -34,6 +34,7 @@ const ServerPluginData = React.lazy(() => import("./views/server/ServerPluginDat
|
||||
const ServerWidePluginData = React.lazy(() => import("./views/server/ServerWidePluginData"));
|
||||
|
||||
const NetworkPage = React.lazy(() => import("./views/layout/NetworkPage"));
|
||||
const NetworkOverview = React.lazy(() => import("./views/network/NetworkOverview"));
|
||||
|
||||
const PlayersPage = React.lazy(() => import("./views/layout/PlayersPage"));
|
||||
const AllPlayers = React.lazy(() => import("./views/players/AllPlayers"));
|
||||
@ -113,6 +114,7 @@ function App() {
|
||||
</Route>
|
||||
<Route path="/network" element={<Lazy><NetworkPage/></Lazy>}>
|
||||
<Route path="" element={<Lazy><OverviewRedirect/></Lazy>}/>
|
||||
<Route path="overview" element={<Lazy><NetworkOverview/></Lazy>}/>
|
||||
<Route path="players" element={<Lazy><AllPlayers/></Lazy>}/>
|
||||
<Route path="plugins-overview" element={<Lazy><ServerPluginData/></Lazy>}/>
|
||||
<Route path="plugins/:plugin" element={<Lazy><ServerWidePluginData/></Lazy>}/>
|
||||
|
@ -3,6 +3,8 @@ import React from "react";
|
||||
import End from "./layout/End";
|
||||
|
||||
const Datapoint = ({icon, color, name, value, valueLabel, bold, boldTitle, title, trend}) => {
|
||||
if (value === undefined && valueLabel === undefined) return <></>;
|
||||
|
||||
const displayedValue = bold ? <b>{value}</b> : value;
|
||||
const extraLabel = typeof valueLabel === 'string' ? ` (${valueLabel})` : '';
|
||||
const colorClass = color && color.startsWith("col-") ? color : "col-" + color;
|
||||
|
@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import CardTabs from "../../../CardTabs";
|
||||
import {faChartArea} from "@fortawesome/free-solid-svg-icons";
|
||||
import {useDataRequest} from "../../../../hooks/dataFetchHook";
|
||||
import {fetchDayByDayGraph, fetchHourByHourGraph, fetchPlayersOnlineGraph} from "../../../../service/serverService";
|
||||
import {ErrorViewBody} from "../../../../views/ErrorView";
|
||||
import {ChartLoader} from "../../../navigation/Loader";
|
||||
import TimeByTimeGraph from "../../../graphs/TimeByTimeGraph";
|
||||
import PlayersOnlineGraph from "../../../graphs/PlayersOnlineGraph";
|
||||
import {useMetadata} from "../../../../hooks/metadataHook";
|
||||
|
||||
const PlayersOnlineTab = () => {
|
||||
const {serverUUID} = useMetadata();
|
||||
const {data, loadingError} = useDataRequest(fetchPlayersOnlineGraph, [serverUUID]);
|
||||
|
||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||
if (!data) return <ChartLoader/>;
|
||||
|
||||
return <PlayersOnlineGraph data={data}/>
|
||||
}
|
||||
|
||||
const DayByDayTab = () => {
|
||||
const {data, loadingError} = useDataRequest(fetchDayByDayGraph, [])
|
||||
|
||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||
if (!data) return <ChartLoader/>;
|
||||
|
||||
return <TimeByTimeGraph data={data}/>
|
||||
}
|
||||
|
||||
const HourByHourTab = () => {
|
||||
const {data, loadingError} = useDataRequest(fetchHourByHourGraph, [])
|
||||
|
||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||
if (!data) return <ChartLoader/>;
|
||||
|
||||
return <TimeByTimeGraph data={data}/>
|
||||
}
|
||||
|
||||
const NetworkOnlineActivityGraphsCard = () => {
|
||||
const {t} = useTranslation();
|
||||
return <Card>
|
||||
<CardTabs tabs={[
|
||||
{
|
||||
name: t('html.label.networkOnlineActivity'), icon: faChartArea, color: 'blue', href: 'online-activity',
|
||||
element: <PlayersOnlineTab/>
|
||||
}, {
|
||||
name: t('html.label.dayByDay'), icon: faChartArea, color: 'blue', href: 'day-by-day',
|
||||
element: <DayByDayTab/>
|
||||
}, {
|
||||
name: t('html.label.hourByHour'), icon: faChartArea, color: 'blue', href: 'hour-by-hour',
|
||||
element: <HourByHourTab/>
|
||||
}
|
||||
]}/>
|
||||
</Card>
|
||||
};
|
||||
|
||||
export default NetworkOnlineActivityGraphsCard
|
@ -31,6 +31,10 @@ const ServerWeekComparisonCard = ({data}) => {
|
||||
text={t('html.label.averagePlaytime') + ' ' + t('html.label.perPlayer')}
|
||||
values={[data.average_playtime_before, data.average_playtime_after,
|
||||
<BigTrend trend={data.average_playtime_trend}/>]}/>
|
||||
<TableRow icon={faClock} color="teal"
|
||||
text={t('html.label.averageSessionLength')}
|
||||
values={[data.session_length_average_before, data.session_length_average_after,
|
||||
<BigTrend trend={data.session_length_average_trend}/>]}/>
|
||||
<TableRow icon={faCalendarCheck} color="teal" text={t('html.label.sessions')}
|
||||
values={[data.sessions_before, data.sessions_after,
|
||||
<BigTrend trend={data.sessions_trend}/>]}/>
|
||||
|
@ -24,7 +24,7 @@ const ServerAsNumbersCard = ({data}) => {
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
<Fa icon={faBookOpen}/> {t('html.label.serverAsNumberse')}
|
||||
<Fa icon={faBookOpen}/> {data.player_kills ? t('html.label.serverAsNumberse') : t('html.label.networkAsNumbers')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
@ -55,10 +55,13 @@ const ServerAsNumbersCard = ({data}) => {
|
||||
<Datapoint name={t('html.label.averagePlaytime') + ' ' + t('html.label.perPlayer')}
|
||||
color={'green'} icon={faClock}
|
||||
value={data.player_playtime}/>
|
||||
<Datapoint name={t('html.label.averageSessionLength')}
|
||||
color={'teal'} icon={faClock}
|
||||
value={data.session_length_avg}/>
|
||||
<Datapoint name={t('html.label.sessions')}
|
||||
color={'teal'} icon={faCalendarCheck}
|
||||
value={data.sessions} bold/>
|
||||
<hr/>
|
||||
{data.player_kills && <hr/>}
|
||||
<Datapoint name={t('html.label.playerKills')}
|
||||
color={'red'} icon={faCrosshairs}
|
||||
value={data.player_kills} bold/>
|
||||
|
@ -3,6 +3,7 @@ import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faAngleRight, faSkullCrossbones} from "@fortawesome/free-solid-svg-icons";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import Scrollable from "../Scrollable";
|
||||
|
||||
const KillRow = ({kill}) => {
|
||||
const killSeparator = <Fa
|
||||
@ -23,16 +24,18 @@ const KillsTable = ({kills}) => {
|
||||
const {nightModeEnabled} = useTheme();
|
||||
|
||||
return (
|
||||
<table className={"table mb-0" + (nightModeEnabled ? " table-dark" : '')}>
|
||||
<tbody>
|
||||
{kills.length ? kills.map((kill, i) => <KillRow key={i} kill={kill}/>) : <tr>
|
||||
<td>{t('html.generic.none')}</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
<Scrollable>
|
||||
<table className={"table mb-0" + (nightModeEnabled ? " table-dark" : '')}>
|
||||
<tbody>
|
||||
{kills.length ? kills.map((kill, i) => <KillRow key={i} kill={kill}/>) : <tr>
|
||||
<td>{t('html.generic.none')}</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</Scrollable>
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -2,6 +2,8 @@ import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import React from "react";
|
||||
|
||||
export const TableRow = ({icon, text, color, values, bold}) => {
|
||||
if (!values || values.filter(value => value !== undefined).length < values.length) return <></>;
|
||||
|
||||
const label = (<><Fa icon={icon} className={'col-' + color}/> {text}</>);
|
||||
return (
|
||||
<tr>
|
||||
|
@ -8,7 +8,7 @@ export const useDataRequest = (fetchMethod, parameters) => {
|
||||
|
||||
/*eslint-disable react-hooks/exhaustive-deps */
|
||||
useEffect(() => {
|
||||
fetchMethod(...parameters, updateRequested).then(({data: json, error}) => {
|
||||
fetchMethod(updateRequested, ...parameters).then(({data: json, error}) => {
|
||||
if (json) {
|
||||
setData(json);
|
||||
finishUpdate(json.timestamp, json.timestamp_f);
|
||||
|
6
Plan/react/dashboard/src/service/networkService.js
Normal file
6
Plan/react/dashboard/src/service/networkService.js
Normal file
@ -0,0 +1,6 @@
|
||||
import {doGetRequest} from "./backendConfiguration";
|
||||
|
||||
export const fetchNetworkOverview = async (updateRequested) => {
|
||||
const url = `/v1/network/overview?timestamp=${updateRequested}`;
|
||||
return doGetRequest(url);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import {faMapSigns} from "@fortawesome/free-solid-svg-icons";
|
||||
import {doSomeGetRequest, standard200option} from "./backendConfiguration";
|
||||
|
||||
export const fetchPlayer = async (uuid, timestamp) => {
|
||||
export const fetchPlayer = async (timestamp, uuid) => {
|
||||
const url = `/v1/player?player=${uuid}×tamp=${timestamp}`;
|
||||
return doSomeGetRequest(url, [
|
||||
standard200option,
|
||||
|
@ -1,133 +1,116 @@
|
||||
import {doGetRequest} from "./backendConfiguration";
|
||||
|
||||
|
||||
export const fetchServerIdentity = async (identifier) => {
|
||||
export const fetchServerIdentity = async (timestamp, identifier) => {
|
||||
const url = `/v1/serverIdentity?server=${identifier}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchServerOverview = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchServerOverview = async (timestamp, identifier) => {
|
||||
const url = `/v1/serverOverview?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchOnlineActivityOverview = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchOnlineActivityOverview = async (timestamp, identifier) => {
|
||||
const url = `/v1/onlineOverview?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPlayerbaseOverview = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchPlayerbaseOverview = async (timestamp, identifier) => {
|
||||
const url = `/v1/playerbaseOverview?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchSessionOverview = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchSessionOverview = async (timestamp, identifier) => {
|
||||
const url = `/v1/sessionsOverview?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPvpPve = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchPvpPve = async (timestamp, identifier) => {
|
||||
const url = `/v1/playerVersus?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPerformanceOverview = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchPerformanceOverview = async (timestamp, identifier) => {
|
||||
const url = `/v1/performanceOverview?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchExtensionData = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchExtensionData = async (timestamp, identifier) => {
|
||||
const url = `/v1/extensionData?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchSessions = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchSessions = async (timestamp, identifier) => {
|
||||
const url = `/v1/sessions?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchKills = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchKills = async (timestamp, identifier) => {
|
||||
const url = `/v1/kills?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPlayers = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchPlayers = async (timestamp, identifier) => {
|
||||
const url = identifier ? `/v1/players?server=${identifier}×tamp=${timestamp}` : `/v1/players?timestamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPingTable = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchPingTable = async (timestamp, identifier) => {
|
||||
const url = identifier ? `/v1/pingTable?server=${identifier}×tamp=${timestamp}` : `/v1/pingTable?timestamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPlayersOnlineGraph = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/graph?type=playersOnline&server=${identifier}×tamp=${timestamp}`;
|
||||
export const fetchPlayersOnlineGraph = async (timestamp, identifier) => {
|
||||
const url = identifier ? `/v1/graph?type=playersOnline&server=${identifier}×tamp=${timestamp}` :
|
||||
`/v1/graph?type=playersOnline×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPlayerbaseDevelopmentGraph = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/graph?type=activity&server=${identifier}×tamp=${timestamp}`;
|
||||
export const fetchPlayerbaseDevelopmentGraph = async (timestamp, identifier) => {
|
||||
const url = identifier ? `/v1/graph?type=activity&server=${identifier}×tamp=${timestamp}` :
|
||||
`/v1/graph?type=activity×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchDayByDayGraph = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/graph?type=uniqueAndNew&server=${identifier}×tamp=${timestamp}`;
|
||||
export const fetchDayByDayGraph = async (timestamp, identifier) => {
|
||||
const url = identifier ? `/v1/graph?type=uniqueAndNew&server=${identifier}×tamp=${timestamp}` :
|
||||
`/v1/graph?type=uniqueAndNew×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchHourByHourGraph = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/graph?type=hourlyUniqueAndNew&server=${identifier}×tamp=${timestamp}`;
|
||||
export const fetchHourByHourGraph = async (timestamp, identifier) => {
|
||||
const url = identifier ? `/v1/graph?type=hourlyUniqueAndNew&server=${identifier}×tamp=${timestamp}` :
|
||||
`/v1/graph?type=hourlyUniqueAndNew×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchServerCalendarGraph = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchServerCalendarGraph = async (timestamp, identifier) => {
|
||||
const url = `/v1/graph?type=serverCalendar&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPunchCardGraph = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchPunchCardGraph = async (timestamp, identifier) => {
|
||||
const url = `/v1/graph?type=punchCard&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchWorldPie = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchWorldPie = async (timestamp, identifier) => {
|
||||
const url = `/v1/graph?type=worldPie&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchGeolocations = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchGeolocations = async (timestamp, identifier) => {
|
||||
const url = `/v1/graph?type=geolocation&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchOptimizedPerformance = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchOptimizedPerformance = async (timestamp, identifier) => {
|
||||
const url = `/v1/graph?type=optimizedPerformance&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPingGraph = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
export const fetchPingGraph = async (timestamp, identifier) => {
|
||||
const url = `/v1/graph?type=aggregatedPing&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
@ -44,7 +44,12 @@ const NetworkSidebar = () => {
|
||||
name: 'html.label.servers',
|
||||
icon: faServer,
|
||||
contents: [
|
||||
{name: 'html.label.overview', icon: faNetworkWired, href: "serversOverview"},
|
||||
{
|
||||
nameShort: 'html.label.overview',
|
||||
name: 'html.label.servers',
|
||||
icon: faNetworkWired,
|
||||
href: "serversOverview"
|
||||
},
|
||||
{name: 'html.label.sessions', icon: faCalendarCheck, href: "sessions"},
|
||||
{name: 'html.label.performance', icon: faCogs, href: "performance"},
|
||||
{},
|
||||
|
@ -20,9 +20,9 @@ const PlayerPage = () => {
|
||||
const {sidebarItems, setSidebarItems} = useNavigation();
|
||||
|
||||
const {identifier} = useParams();
|
||||
const {currentTab, updateRequested, finishUpdate} = useNavigation();
|
||||
const {currentTab, finishUpdate} = useNavigation();
|
||||
|
||||
const {data: player, loadingError} = useDataRequest(fetchPlayer, [identifier, updateRequested])
|
||||
const {data: player, loadingError} = useDataRequest(fetchPlayer, [identifier])
|
||||
|
||||
useEffect(() => {
|
||||
if (!player) return;
|
||||
|
80
Plan/react/dashboard/src/views/network/NetworkOverview.js
Normal file
80
Plan/react/dashboard/src/views/network/NetworkOverview.js
Normal file
@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import {useDataRequest} from "../../hooks/dataFetchHook";
|
||||
import ErrorView from "../ErrorView";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import {Card, Col, Row} from "react-bootstrap-v5";
|
||||
import ServerAsNumbersCard from "../../components/cards/server/values/ServerAsNumbersCard";
|
||||
import ServerWeekComparisonCard from "../../components/cards/server/tables/ServerWeekComparisonCard";
|
||||
import {fetchNetworkOverview} from "../../service/networkService";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {CardLoader} from "../../components/navigation/Loader";
|
||||
import Datapoint from "../../components/Datapoint";
|
||||
import {faUsers} from "@fortawesome/free-solid-svg-icons";
|
||||
import NetworkOnlineActivityGraphsCard from "../../components/cards/server/graphs/NetworkOnlineActivityGraphsCard";
|
||||
|
||||
|
||||
const RecentPlayersCard = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
if (!data) return <CardLoader/>;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
{t('html.label.players')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<p>{t('html.label.last24hours')}</p>
|
||||
<Datapoint icon={faUsers} color="light-blue"
|
||||
name={t('html.label.uniquePlayers')} value={data.unique_players_1d}/>
|
||||
<Datapoint icon={faUsers} color="light-green"
|
||||
name={t('html.label.newPlayers')} value={data.new_players_1d}/>
|
||||
<p>{t('html.label.last7days')}</p>
|
||||
<Datapoint icon={faUsers} color="light-blue"
|
||||
name={t('html.label.uniquePlayers')} value={data.unique_players_7d}/>
|
||||
<Datapoint icon={faUsers} color="light-green"
|
||||
name={t('html.label.newPlayers')} value={data.new_players_7d}/>
|
||||
<p>{t('html.label.last30days')}</p>
|
||||
<Datapoint icon={faUsers} color="light-blue"
|
||||
name={t('html.label.uniquePlayers')} value={data.unique_players_30d}/>
|
||||
<Datapoint icon={faUsers} color="light-green"
|
||||
name={t('html.label.newPlayers')} value={data.new_players_30d}/>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const NetworkOverview = () => {
|
||||
const {data, loadingError} = useDataRequest(fetchNetworkOverview, [])
|
||||
|
||||
if (loadingError) {
|
||||
return <ErrorView error={loadingError}/>
|
||||
}
|
||||
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className="network_overview">
|
||||
<Row>
|
||||
<Col lg={9}>
|
||||
<NetworkOnlineActivityGraphsCard/>
|
||||
</Col>
|
||||
<Col lg={3}>
|
||||
<RecentPlayersCard data={data?.players}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col lg={4}>
|
||||
<ServerAsNumbersCard data={data?.numbers}/>
|
||||
</Col>
|
||||
<Col lg={8}>
|
||||
<ServerWeekComparisonCard data={data?.weeks}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
};
|
||||
|
||||
export default NetworkOverview
|
Loading…
Reference in New Issue
Block a user