mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-12-24 01:58:25 +01:00
React PageExtension Javascript APIs (#2894)
* Page extension api javascript file * Add support for extending rows with pageExtensionApi Tested it * Support javascript snippets in react index.html * Add context to pageExtensionApi calls * Fix redirect text overlapping with sidebar * Add new API for registering custom javascript and css
This commit is contained in:
parent
154d0b2b46
commit
b92e886a39
@ -16,6 +16,8 @@
|
||||
*/
|
||||
package com.djrapitops.plan.capability;
|
||||
|
||||
import com.djrapitops.plan.delivery.web.ResourceService;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@ -83,7 +85,12 @@ enum Capability {
|
||||
/**
|
||||
* {@link com.djrapitops.plan.delivery.web.ResourceService}
|
||||
*/
|
||||
PAGE_EXTENSION_RESOURCES;
|
||||
PAGE_EXTENSION_RESOURCES,
|
||||
/**
|
||||
* {@link com.djrapitops.plan.delivery.web.ResourceService#addJavascriptToResource(String, String, ResourceService.Position, String, String)}
|
||||
* {@link com.djrapitops.plan.delivery.web.ResourceService#addStyleToResource(String, String, ResourceService.Position, String, String)}
|
||||
*/
|
||||
PAGE_EXTENSION_RESOURCES_REGISTER_DIRECT_CUSTOMIZATION;
|
||||
|
||||
static Optional<Capability> getByName(String name) {
|
||||
if (name == null) {
|
||||
|
@ -16,6 +16,9 @@
|
||||
*/
|
||||
package com.djrapitops.plan.delivery.web;
|
||||
|
||||
import com.djrapitops.plan.delivery.web.resolver.MimeType;
|
||||
import com.djrapitops.plan.delivery.web.resolver.NoAuthResolver;
|
||||
import com.djrapitops.plan.delivery.web.resolver.Response;
|
||||
import com.djrapitops.plan.delivery.web.resource.WebResource;
|
||||
|
||||
import java.util.Optional;
|
||||
@ -48,7 +51,7 @@ public interface ResourceService {
|
||||
WebResource getResource(String pluginName, String fileName, Supplier<WebResource> source);
|
||||
|
||||
/**
|
||||
* Add javascript to load in an existing html resource.
|
||||
* Add javascript to load in a html resource.
|
||||
* <p>
|
||||
* Adds {@code <script src="jsSrc"></script>} or multiple to the resource.
|
||||
*
|
||||
@ -63,6 +66,40 @@ public interface ResourceService {
|
||||
*/
|
||||
void addScriptsToResource(String pluginName, String fileName, Position position, String... jsSources);
|
||||
|
||||
/**
|
||||
* Add javascript to load in a html resource.
|
||||
* <p>
|
||||
* Requires PAGE_EXTENSION_RESOURCES_REGISTER_DIRECT_CUSTOMIZATION Capability.
|
||||
*
|
||||
* @param pluginName Name of your plugin (for config purposes)
|
||||
* @param fileName Name of the .html file being modified
|
||||
* @param position Where to place the script tag on the page.
|
||||
* @param scriptName Name of your javascript file (used on the page)
|
||||
* @param javascriptAsString Javascript file contents in UTF-8
|
||||
* @throws IllegalArgumentException If fileName is null, empty or does not end with .html
|
||||
* @throws IllegalArgumentException If anything is empty or null
|
||||
*/
|
||||
default void addJavascriptToResource(String pluginName, String fileName, Position position, String scriptName, String javascriptAsString) {
|
||||
if (javascriptAsString == null || javascriptAsString.isEmpty()) {
|
||||
throw new IllegalArgumentException("null or empty 'javascriptAsString' given.");
|
||||
}
|
||||
if (scriptName == null || scriptName.isEmpty()) {
|
||||
throw new IllegalArgumentException("null or empty 'scriptName' given.");
|
||||
}
|
||||
String actualScriptName = scriptName.endsWith(".js") ? scriptName : scriptName + ".js";
|
||||
|
||||
addScriptsToResource(pluginName, fileName, position, pluginName + "/" + actualScriptName);
|
||||
ResolverService.getInstance().registerResolver(pluginName, pluginName + "/" + actualScriptName, (NoAuthResolver) request -> {
|
||||
if (request.getPath().asString().equals(actualScriptName)) {
|
||||
return Optional.of(Response.builder()
|
||||
.setContent(javascriptAsString)
|
||||
.setMimeType(MimeType.JS)
|
||||
.build());
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add css to load in an existing html resource.
|
||||
* <p>
|
||||
@ -79,9 +116,46 @@ public interface ResourceService {
|
||||
*/
|
||||
void addStylesToResource(String pluginName, String fileName, Position position, String... cssSources);
|
||||
|
||||
|
||||
/**
|
||||
* Add javascript to load in a html resource.
|
||||
* <p>
|
||||
* Requires PAGE_EXTENSION_RESOURCES_REGISTER_DIRECT_CUSTOMIZATION Capability.
|
||||
*
|
||||
* @param pluginName Name of your plugin (for config purposes)
|
||||
* @param fileName Name of the .html file being modified
|
||||
* @param position Where to place the script tag on the page.
|
||||
* @param cssFileName Name of your css file (used on the page)
|
||||
* @param cssAsString CSS file contents in UTF-8
|
||||
* @throws IllegalArgumentException If fileName is null, empty or does not end with .html
|
||||
* @throws IllegalArgumentException If anything is empty or null
|
||||
*/
|
||||
default void addStyleToResource(String pluginName, String fileName, Position position, String cssFileName, String cssAsString) {
|
||||
if (cssAsString == null || cssAsString.isEmpty()) {
|
||||
throw new IllegalArgumentException("null or empty 'cssAsString' given.");
|
||||
}
|
||||
if (cssFileName == null || cssFileName.isEmpty()) {
|
||||
throw new IllegalArgumentException("null or empty 'cssFileName' given.");
|
||||
}
|
||||
String actualCssFileName = cssFileName.endsWith(".js") ? cssFileName : cssFileName + ".js";
|
||||
|
||||
addStylesToResource(pluginName, fileName, position, pluginName + "/" + actualCssFileName);
|
||||
ResolverService.getInstance().registerResolver(pluginName, pluginName + "/" + actualCssFileName, (NoAuthResolver) request -> {
|
||||
if (request.getPath().asString().equals(actualCssFileName)) {
|
||||
return Optional.of(Response.builder()
|
||||
.setContent(cssAsString)
|
||||
.setMimeType(MimeType.CSS)
|
||||
.build());
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
enum Position {
|
||||
/**
|
||||
* Loaded before page contents.
|
||||
* <p>
|
||||
* Recommended for loading style sheets.
|
||||
*/
|
||||
PRE_CONTENT,
|
||||
/**
|
||||
@ -94,7 +168,11 @@ public interface ResourceService {
|
||||
* Loaded after script execution.
|
||||
* <p>
|
||||
* Recommended for loading data to custom structure on the page.
|
||||
*
|
||||
* @see <a href="https://github.com/plan-player-analytics/Plan/blob/master/Plan/react/dashboard/public/pageExtensionApi.js">Javascript API</a>
|
||||
* @deprecated No longer supported on React pages, use the javascript API in PRE_MAIN_SCRIPT.
|
||||
*/
|
||||
@Deprecated
|
||||
AFTER_MAIN_SCRIPT;
|
||||
|
||||
public String cleanName() {
|
||||
|
@ -74,6 +74,7 @@ public class ReactExporter extends FileExporter {
|
||||
exportAsset(toDirectory, "logo512.png");
|
||||
exportAsset(toDirectory, "manifest.json");
|
||||
exportAsset(toDirectory, "robots.txt");
|
||||
exportAsset(toDirectory, "pageExtensionApi.js");
|
||||
exportStaticBundle(toDirectory);
|
||||
exportLocaleJson(toDirectory.resolve("locale"));
|
||||
exportMetadataJson(toDirectory.resolve("metadata"));
|
||||
@ -140,7 +141,9 @@ public class ReactExporter extends FileExporter {
|
||||
String contents = files.getResourceFromJar("web/index.html")
|
||||
.asString();
|
||||
String basePath = getBasePath();
|
||||
contents = StringUtils.replace(contents, "/static", basePath + "/static");
|
||||
contents = StringUtils.replaceEach(contents,
|
||||
new String[]{"/static", "/pageExtensionApi.js"},
|
||||
new String[]{basePath + "/static", basePath + "/pageExtensionApi.js"});
|
||||
|
||||
export(toDirectory.resolve("index.html"), contents);
|
||||
}
|
||||
|
@ -109,8 +109,15 @@ public class PageFactory {
|
||||
}
|
||||
|
||||
public Page reactPage() throws IOException {
|
||||
// TODO use ResourceService to apply snippets to the React index.html
|
||||
return new ReactPage(getBasePath(), getPublicHtmlOrJarResource("index.html"));
|
||||
try {
|
||||
String fileName = "index.html";
|
||||
WebResource resource = ResourceService.getInstance().getResource(
|
||||
"Plan", fileName, () -> getPublicHtmlOrJarResource(fileName)
|
||||
);
|
||||
return new ReactPage(getBasePath(), resource);
|
||||
} catch (UncheckedIOException readFail) {
|
||||
throw readFail.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
private String getBasePath() {
|
||||
@ -258,14 +265,10 @@ public class PageFactory {
|
||||
}
|
||||
}
|
||||
|
||||
public WebResource getPublicHtmlOrJarResource(String resourceName) throws IOException {
|
||||
try {
|
||||
return publicHtmlFiles.get().findPublicHtmlResource(resourceName)
|
||||
.orElseGet(() -> files.get().getResourceFromJar("web/" + resourceName))
|
||||
.asWebResource();
|
||||
} catch (UncheckedIOException readFail) {
|
||||
throw readFail.getCause();
|
||||
}
|
||||
public WebResource getPublicHtmlOrJarResource(String resourceName) {
|
||||
return publicHtmlFiles.get().findPublicHtmlResource(resourceName)
|
||||
.orElseGet(() -> files.get().getResourceFromJar("web/" + resourceName))
|
||||
.asWebResource();
|
||||
}
|
||||
|
||||
public Page loginPage() throws IOException {
|
||||
|
@ -36,9 +36,10 @@ public class ReactPage implements Page {
|
||||
|
||||
@Override
|
||||
public String toHtml() {
|
||||
return StringUtils.replace(
|
||||
return StringUtils.replaceEach(
|
||||
reactHtml.asString(),
|
||||
"/static", basePath + "/static");
|
||||
new String[]{"/static", "/pageExtensionApi.js"},
|
||||
new String[]{basePath + "/static", basePath + "/pageExtensionApi.js"});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -29,6 +29,7 @@ import com.djrapitops.plan.utilities.logging.ErrorLogger;
|
||||
import net.playeranalytics.plugin.server.PluginLogger;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.text.TextStringBuilder;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
@ -94,20 +95,8 @@ public class ResourceSvc implements ResourceService {
|
||||
}
|
||||
}
|
||||
|
||||
private WebResource applySnippets(String pluginName, @Untrusted String fileName, WebResource resource) {
|
||||
Map<Position, StringBuilder> byPosition = calculateSnippets(fileName);
|
||||
if (byPosition.isEmpty()) return resource;
|
||||
|
||||
String html = applySnippets(resource, byPosition);
|
||||
return WebResource.create(html);
|
||||
}
|
||||
|
||||
private String applySnippets(WebResource resource, Map<Position, StringBuilder> byPosition) {
|
||||
String html = resource.asString();
|
||||
if (html == null) {
|
||||
return "Error: Given resource did not support WebResource#asString method properly and returned 'null'";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String applyLegacy(Map<Position, StringBuilder> byPosition, String html) {
|
||||
StringBuilder toHead = byPosition.get(Position.PRE_CONTENT);
|
||||
if (toHead != null) {
|
||||
html = StringUtils.replaceOnce(html, "</head>", toHead.append("</head>").toString());
|
||||
@ -130,6 +119,33 @@ public class ResourceSvc implements ResourceService {
|
||||
return html;
|
||||
}
|
||||
|
||||
private WebResource applySnippets(String pluginName, @Untrusted String fileName, WebResource resource) {
|
||||
Map<Position, StringBuilder> byPosition = calculateSnippets(fileName);
|
||||
if (byPosition.isEmpty()) return resource;
|
||||
|
||||
String html = applySnippets(fileName, resource, byPosition);
|
||||
return WebResource.create(html);
|
||||
}
|
||||
|
||||
private String applySnippets(@Untrusted String fileName, WebResource resource, Map<Position, StringBuilder> byPosition) {
|
||||
String html = resource.asString();
|
||||
if (html == null) {
|
||||
return "Error: Given resource did not support WebResource#asString method properly and returned 'null'";
|
||||
}
|
||||
|
||||
if ("index.html".equals(fileName)) {
|
||||
StringBuilder toHead = byPosition.get(Position.PRE_CONTENT);
|
||||
if (toHead != null) {
|
||||
html = StringUtils.replaceOnce(html, "</head>", toHead.append("</head>").toString());
|
||||
}
|
||||
StringBuilder toBody = byPosition.get(Position.PRE_MAIN_SCRIPT);
|
||||
html = StringUtils.replaceOnce(html, "<script>/* This script tag will be replaced with scripts */</script>", toBody.toString());
|
||||
return html;
|
||||
} else {
|
||||
return applyLegacy(byPosition, html);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Position, StringBuilder> calculateSnippets(@Untrusted String fileName) {
|
||||
Map<Position, StringBuilder> byPosition = new EnumMap<>(Position.class);
|
||||
for (Snippet snippet : snippets) {
|
||||
|
@ -143,6 +143,7 @@ public class ResponseResolver {
|
||||
resolverService.registerResolver(plugin, "/manifest.json", fileResolver(() -> responseFactory.jsonFileResponse("manifest.json")));
|
||||
resolverService.registerResolver(plugin, "/asset-manifest.json", fileResolver(() -> responseFactory.jsonFileResponse("asset-manifest.json")));
|
||||
resolverService.registerResolver(plugin, "/favicon.ico", fileResolver(responseFactory::faviconResponse));
|
||||
resolverService.registerResolver(plugin, "/pageExtensionApi.js", fileResolver(() -> responseFactory.javaScriptResponse("pageExtensionApi.js")));
|
||||
|
||||
resolverService.registerResolver(plugin, "/query", queryPageResolver);
|
||||
resolverService.registerResolver(plugin, "/players", playersPageResolver);
|
||||
|
@ -219,6 +219,7 @@ class AccessControlTest {
|
||||
"/v1/locale/NonexistingLanguage,404",
|
||||
"/docs/swagger.json,500", // swagger.json not available during tests
|
||||
"/docs,200",
|
||||
"/pageExtensionApi.js,200",
|
||||
})
|
||||
void levelZeroCanAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException {
|
||||
int responseCode = access(resource, cookieLevel0);
|
||||
@ -293,6 +294,7 @@ class AccessControlTest {
|
||||
"/v1/locale/NonexistingLanguage,404",
|
||||
"/docs/swagger.json,403",
|
||||
"/docs,403",
|
||||
"/pageExtensionApi.js,200",
|
||||
})
|
||||
void levelOneCanAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException {
|
||||
int responseCode = access(resource, cookieLevel1);
|
||||
@ -367,6 +369,7 @@ class AccessControlTest {
|
||||
"/v1/locale/NonexistingLanguage,404",
|
||||
"/docs/swagger.json,403",
|
||||
"/docs,403",
|
||||
"/pageExtensionApi.js,200",
|
||||
})
|
||||
void levelTwoCanAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException {
|
||||
int responseCode = access(resource, cookieLevel2);
|
||||
@ -439,6 +442,7 @@ class AccessControlTest {
|
||||
"/v1/locale/NonexistingLanguage,404",
|
||||
"/docs/swagger.json,403",
|
||||
"/docs,403",
|
||||
"/pageExtensionApi.js,200",
|
||||
})
|
||||
void levelHundredCanNotAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException {
|
||||
int responseCode = access(resource, cookieLevel100);
|
||||
|
@ -1,34 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
|
||||
<meta content="Player Analytics" name="description">
|
||||
<meta content="AuroraLS3" name="author">
|
||||
<meta content="noindex, nofollow" name="robots">
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
|
||||
<meta content="Player Analytics" name="description">
|
||||
<meta content="AuroraLS3" name="author">
|
||||
<meta content="noindex, nofollow" name="robots">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link href="%PUBLIC_URL%/manifest.json" rel="manifest"/>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link href="%PUBLIC_URL%/manifest.json" rel="manifest"/>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Plan | Player Analytics</title>
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Plan | Player Analytics</title>
|
||||
|
||||
|
||||
<link crossorigin="anonymous"
|
||||
href="https://fonts.googleapis.com/css?family=Nunito:400,700,800,900&display=swap&subset=latin-ext"
|
||||
rel="stylesheet">
|
||||
<link crossorigin="anonymous"
|
||||
href="https://fonts.googleapis.com/css?family=Nunito:400,700,800,900&display=swap&subset=latin-ext"
|
||||
rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<script src="%PUBLIC_URL%/pageExtensionApi.js"></script>
|
||||
<script>/* This script tag will be replaced with scripts */</script>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
69
Plan/react/dashboard/public/pageExtensionApi.js
Normal file
69
Plan/react/dashboard/public/pageExtensionApi.js
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Usage:
|
||||
* - Look for any element with 'extendable' class name and use its id.
|
||||
*
|
||||
* // Html-string
|
||||
* const render = async (context) => {
|
||||
* // Any code that needs to run before the element is added to DOM
|
||||
* return '<p>Hello world</p>';
|
||||
* }
|
||||
* const unmount = async () => {
|
||||
* // Any code that needs to run when the element is removed from DOM
|
||||
* };
|
||||
* // 'server-overview-card' is the ID of the element
|
||||
* global.pageExtensionApi.registerElement('beforeElement', 'server-overview-card', render, unmount);
|
||||
*
|
||||
* // HtmlElement
|
||||
* global.pageExtensionApi.registerElement('afterElement', 'server-overview-card', () => {
|
||||
* const para = document.createElement("p");
|
||||
* para.innerText = "Hello world";
|
||||
* return para;
|
||||
* }, unmount);
|
||||
*/
|
||||
class PageExtensionApi {
|
||||
beforeElementRenders = [];
|
||||
afterElementRenders = [];
|
||||
|
||||
registerElement(position, id, renderCallback, unmountCallback) {
|
||||
const renderers = position === 'beforeElement' ? this.beforeElementRenders : this.afterElementRenders;
|
||||
renderers.push({id, renderCallback, unmountCallback});
|
||||
}
|
||||
|
||||
onRender(id, position, context) {
|
||||
const renderers = position === 'beforeElement' ? this.beforeElementRenders : this.afterElementRenders;
|
||||
return Promise.all(renderers
|
||||
.filter(renderer => renderer.id === id)
|
||||
.filter(renderer => renderer.renderCallback)
|
||||
.map(async renderer => {
|
||||
try {
|
||||
const rendered = await renderer.renderCallback(context);
|
||||
if (rendered instanceof HTMLElement) {
|
||||
return rendered.outerHTML;
|
||||
} else {
|
||||
return rendered;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Error when rendering " + renderer + ": " + e);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(renderedElement => renderedElement));
|
||||
}
|
||||
|
||||
onUnmount(className, position) {
|
||||
const renderers = position === 'beforeElement' ? this.beforeElementRenders : this.afterElementRenders;
|
||||
return renderers
|
||||
.filter(renderer => renderer.className === className)
|
||||
.forEach(renderer => {
|
||||
try {
|
||||
return renderer.unmountCallback()
|
||||
} catch (e) {
|
||||
console.warn("Error when unmounting " + renderer + ": " + e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Global
|
||||
pageExtensionApi = new PageExtensionApi();
|
@ -14,6 +14,7 @@ import {AuthenticationContextProvider} from "./hooks/authenticationHook";
|
||||
import {NavigationContextProvider} from "./hooks/navigationHook";
|
||||
import MainPageRedirect from "./components/navigation/MainPageRedirect";
|
||||
import {baseAddress, staticSite} from "./service/backendConfiguration";
|
||||
import {PageExtensionContextProvider} from "./hooks/pageExtensionHook";
|
||||
|
||||
const PlayerPage = React.lazy(() => import("./views/layout/PlayerPage"));
|
||||
const PlayerOverview = React.lazy(() => import("./views/player/PlayerOverview"));
|
||||
@ -69,7 +70,9 @@ const ContextProviders = ({children}) => (
|
||||
<MetadataContextProvider>
|
||||
<ThemeContextProvider>
|
||||
<NavigationContextProvider>
|
||||
{children}
|
||||
<PageExtensionContextProvider>
|
||||
{children}
|
||||
</PageExtensionContextProvider>
|
||||
</NavigationContextProvider>
|
||||
</ThemeContextProvider>
|
||||
</MetadataContextProvider>
|
||||
|
@ -1,11 +1,12 @@
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Card, Col, Dropdown, Row} from "react-bootstrap";
|
||||
import {Card, Col, Dropdown} from "react-bootstrap";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import React, {useState} from "react";
|
||||
import {faExclamationTriangle, faGlobe, faLayerGroup} from "@fortawesome/free-solid-svg-icons";
|
||||
import GeolocationBarGraph from "../../graphs/GeolocationBarGraph";
|
||||
import GeolocationWorldMap, {ProjectionOptions} from "../../graphs/GeolocationWorldMap";
|
||||
import {CardLoader} from "../../navigation/Loader";
|
||||
import ExtendableRow from "../../layout/extension/ExtendableRow";
|
||||
|
||||
const ProjectionDropDown = ({projection, setProjection}) => {
|
||||
const {t} = useTranslation();
|
||||
@ -54,7 +55,7 @@ const GeolocationsCard = ({data}) => {
|
||||
<ProjectionDropDown projection={projection} setProjection={setProjection}/>
|
||||
</Card.Header>
|
||||
<Card.Body className="chart-area" style={{height: "100%"}}>
|
||||
<Row>
|
||||
<ExtendableRow id={'row-geolocations-graphs-card-0'}>
|
||||
<Col md={3}>
|
||||
<GeolocationBarGraph series={data.geolocation_bar_series} color={data.colors.bars}/>
|
||||
</Col>
|
||||
@ -62,7 +63,7 @@ const GeolocationsCard = ({data}) => {
|
||||
<GeolocationWorldMap series={data.geolocation_series} colors={data.colors}
|
||||
projection={projection}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)
|
||||
|
@ -14,6 +14,7 @@ import Datapoint from "../../../Datapoint";
|
||||
import {faCalendarCheck, faClock} from "@fortawesome/free-regular-svg-icons";
|
||||
import React from "react";
|
||||
import {CardLoader} from "../../../navigation/Loader";
|
||||
import ExtendableCardBody from "../../../layout/extension/ExtendableCardBody";
|
||||
|
||||
const ServerAsNumbersCard = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
@ -27,7 +28,7 @@ const ServerAsNumbersCard = ({data}) => {
|
||||
<Fa icon={faBookOpen}/> {data.player_kills ? t('html.label.serverAsNumberse') : t('html.label.networkAsNumbers')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<ExtendableCardBody id={data.player_kills ? 'card-body-server-as-numbers' : 'card-body-network-as-numbers'}>
|
||||
<Datapoint name={t('html.label.currentUptime')}
|
||||
color={'light-green'} icon={faPowerOff}
|
||||
value={data.current_uptime}/>
|
||||
@ -71,7 +72,7 @@ const ServerAsNumbersCard = ({data}) => {
|
||||
<Datapoint name={t('html.label.deaths')}
|
||||
color={'black'} icon={faSkull}
|
||||
value={data.deaths} bold/>
|
||||
</Card.Body>
|
||||
</ExtendableCardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {Card} from "react-bootstrap";
|
||||
import {usePageExtension} from "../../../hooks/pageExtensionHook";
|
||||
|
||||
const ExtendableCardBody = ({id, className, children}) => {
|
||||
const [elementsBefore, setElementsBefore] = useState([]);
|
||||
const [elementsAfter, setElementsAfter] = useState([]);
|
||||
const {onRender, onUnmount, context} = usePageExtension();
|
||||
|
||||
const render = useCallback(async () => {
|
||||
if (!onRender) return;
|
||||
setElementsBefore(await onRender(id, 'beforeElement', context));
|
||||
setElementsAfter(await onRender(id, 'afterElement', context));
|
||||
}, [setElementsBefore, setElementsAfter, id, onRender, context])
|
||||
useEffect(() => {
|
||||
render();
|
||||
|
||||
return () => {
|
||||
if (!onUnmount) return;
|
||||
setElementsBefore([])
|
||||
setElementsAfter([])
|
||||
onUnmount(id, 'beforeElement');
|
||||
onUnmount(id, 'afterElement');
|
||||
}
|
||||
}, [setElementsBefore, setElementsAfter, id, onUnmount, render]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div dangerouslySetInnerHTML={{__html: elementsBefore.join('')}}/>
|
||||
<Card.Body id={id} className={className ? "extendable " + className : "extendable"}>
|
||||
{children}
|
||||
</Card.Body>
|
||||
<div dangerouslySetInnerHTML={{__html: elementsAfter.join('')}}/>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export default ExtendableCardBody
|
@ -0,0 +1,38 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {Row} from "react-bootstrap";
|
||||
import {usePageExtension} from "../../../hooks/pageExtensionHook";
|
||||
|
||||
const ExtendableRow = ({id, className, children}) => {
|
||||
const [elementsBefore, setElementsBefore] = useState([]);
|
||||
const [elementsAfter, setElementsAfter] = useState([]);
|
||||
const {onRender, onUnmount, context} = usePageExtension();
|
||||
|
||||
const render = useCallback(async () => {
|
||||
if (!onRender) return;
|
||||
setElementsBefore(await onRender(id, 'beforeElement', context));
|
||||
setElementsAfter(await onRender(id, 'afterElement', context));
|
||||
}, [setElementsBefore, setElementsAfter, id, onRender, context])
|
||||
useEffect(() => {
|
||||
render();
|
||||
|
||||
return () => {
|
||||
if (!onUnmount) return;
|
||||
setElementsBefore([])
|
||||
setElementsAfter([])
|
||||
onUnmount(id, 'beforeElement');
|
||||
onUnmount(id, 'afterElement');
|
||||
}
|
||||
}, [setElementsBefore, setElementsAfter, id, onUnmount, render]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div dangerouslySetInnerHTML={{__html: elementsBefore.join('')}}/>
|
||||
<Row id={id} className={className ? "extendable " + className : "extendable"}>
|
||||
{children}
|
||||
</Row>
|
||||
<div dangerouslySetInnerHTML={{__html: elementsAfter.join('')}}/>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export default ExtendableRow
|
@ -21,6 +21,7 @@ const RedirectPlaceholder = () => {
|
||||
|
||||
if (dateDiff > 50) {
|
||||
return <>
|
||||
<p style={{marginLeft: "14rem"}}></p>
|
||||
<p className="m-4">Redirecting..</p>
|
||||
<div style={{maxWidth: "500px"}}>
|
||||
<p className="m-4">
|
||||
@ -32,7 +33,7 @@ const RedirectPlaceholder = () => {
|
||||
</p>
|
||||
<p className="m-4">
|
||||
If you are trying to set up a development environment,
|
||||
change package.json "proxy" to your Plan webserver address.
|
||||
change package.json "proxy" to address of your Plan webserver.
|
||||
</p>
|
||||
<p className="m-4">
|
||||
<button className="btn bg-plan" onClick={() => window.location.reload()}>Click to Refresh the
|
||||
@ -42,7 +43,10 @@ const RedirectPlaceholder = () => {
|
||||
</div>
|
||||
</>
|
||||
} else {
|
||||
return <p className="m-4">Redirecting..</p>
|
||||
return <>
|
||||
<p style={{marginLeft: "14rem"}}></p>
|
||||
<p className="m-4">Redirecting..</p>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
|
61
Plan/react/dashboard/src/hooks/pageExtensionHook.js
Normal file
61
Plan/react/dashboard/src/hooks/pageExtensionHook.js
Normal file
@ -0,0 +1,61 @@
|
||||
import {createContext, useCallback, useContext, useMemo} from "react";
|
||||
import {useAuth} from "./authenticationHook";
|
||||
import {useNavigation} from "./navigationHook";
|
||||
import {useTheme} from "./themeHook";
|
||||
import {useMetadata} from "./metadataHook";
|
||||
import {getColors, withReducedSaturation} from "../util/colors";
|
||||
import axios from "axios";
|
||||
|
||||
const PageExtensionContext = createContext({});
|
||||
|
||||
export const PageExtensionContextProvider = ({children}) => {
|
||||
const onRender = useCallback(async (className, position) => {
|
||||
return await global.pageExtensionApi.onRender(className, position);
|
||||
}, []);
|
||||
|
||||
const onUnmount = useCallback((className, position) => {
|
||||
global.pageExtensionApi.onUnmount(className, position);
|
||||
}, []);
|
||||
|
||||
const authContext = useAuth();
|
||||
const navigationContext = useNavigation();
|
||||
const themeContext = useTheme();
|
||||
const metadata = useMetadata();
|
||||
const callContext = useMemo(() => {
|
||||
return {
|
||||
authentication: {
|
||||
loggedIn: authContext.loggedIn,
|
||||
user: authContext.user,
|
||||
hasPermission: authContext.hasPermission
|
||||
},
|
||||
navigation: {
|
||||
currentTab: navigationContext.currentTab
|
||||
},
|
||||
theme: {
|
||||
currentThemeColor: themeContext.selectedColor,
|
||||
colorMap: getColors(),
|
||||
withReducedSaturation
|
||||
},
|
||||
metadata: {
|
||||
...metadata
|
||||
},
|
||||
utilities: {
|
||||
axios
|
||||
}
|
||||
};
|
||||
}, [authContext, navigationContext, themeContext, metadata]);
|
||||
|
||||
const sharedState = useMemo(() => {
|
||||
return {
|
||||
onRender, onUnmount, context: callContext
|
||||
};
|
||||
}, [onRender, onUnmount, callContext]);
|
||||
return (<PageExtensionContext.Provider value={sharedState}>
|
||||
{children}
|
||||
</PageExtensionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const usePageExtension = () => {
|
||||
return useContext(PageExtensionContext);
|
||||
}
|
@ -1,21 +1,22 @@
|
||||
import React from 'react';
|
||||
import {Col, Row} from "react-bootstrap";
|
||||
import {Col} from "react-bootstrap";
|
||||
import {ErrorViewCard} from "../ErrorView";
|
||||
import GeolocationsCard from "../../components/cards/common/GeolocationsCard";
|
||||
import PingTableCard from "../../components/cards/common/PingTableCard";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const Geolocations = ({className, geolocationData, pingData, geolocationError, pingError}) => {
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className={className}>
|
||||
<Row>
|
||||
<ExtendableRow id={'row-' + className}>
|
||||
<Col md={12}>
|
||||
{geolocationError ? <ErrorViewCard error={geolocationError}/> :
|
||||
<GeolocationsCard data={geolocationData}/>}
|
||||
{pingError ? <ErrorViewCard error={pingError}/> : <PingTableCard data={pingData}/>}
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -9,7 +9,7 @@ const NetworkGeolocations = () => {
|
||||
const {data: pingData, loadingError: pingLoadingError} = useDataRequest(fetchNetworkPingTable, []);
|
||||
|
||||
return (
|
||||
<Geolocations className={"network_geolocations"}
|
||||
<Geolocations className={"network-geolocations"}
|
||||
geolocationData={data} geolocationError={loadingError}
|
||||
pingData={pingData} pingError={pingLoadingError}
|
||||
/>
|
||||
|
@ -1,21 +1,22 @@
|
||||
import React from 'react';
|
||||
import {Col, Row} from "react-bootstrap";
|
||||
import {Col} from "react-bootstrap";
|
||||
import JoinAddressGroupCard from "../../components/cards/server/graphs/JoinAddressGroupCard";
|
||||
import JoinAddressGraphCard from "../../components/cards/server/graphs/JoinAddressGraphCard";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const NetworkJoinAddresses = () => {
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className={"network_join_addresses"}>
|
||||
<Row>
|
||||
<section className={"network-join-addresses"}>
|
||||
<ExtendableRow id={'row-network-join-addresses-0'}>
|
||||
<Col lg={8}>
|
||||
<JoinAddressGraphCard identifier={undefined}/>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<JoinAddressGroupCard identifier={undefined}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -2,7 +2,7 @@ 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";
|
||||
import {Card, Col} from "react-bootstrap";
|
||||
import ServerAsNumbersCard from "../../components/cards/server/values/ServerAsNumbersCard";
|
||||
import ServerWeekComparisonCard from "../../components/cards/server/tables/ServerWeekComparisonCard";
|
||||
import {fetchNetworkOverview} from "../../service/networkService";
|
||||
@ -11,6 +11,8 @@ 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";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
import ExtendableCardBody from "../../components/layout/extension/ExtendableCardBody";
|
||||
|
||||
|
||||
const RecentPlayersCard = ({data}) => {
|
||||
@ -25,7 +27,7 @@ const RecentPlayersCard = ({data}) => {
|
||||
{t('html.label.players')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<ExtendableCardBody id={'card-body-network-overview-players'}>
|
||||
<p>{t('html.label.last24hours')}</p>
|
||||
<Datapoint icon={faUsers} color="light-blue"
|
||||
name={t('html.label.uniquePlayers')} value={data.unique_players_1d}/>
|
||||
@ -41,7 +43,7 @@ const RecentPlayersCard = ({data}) => {
|
||||
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>
|
||||
</ExtendableCardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@ -56,22 +58,22 @@ const NetworkOverview = () => {
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className="network_overview">
|
||||
<Row>
|
||||
<ExtendableRow id={'row-network-overview-0'}>
|
||||
<Col lg={9}>
|
||||
<NetworkOnlineActivityGraphsCard/>
|
||||
</Col>
|
||||
<Col lg={3}>
|
||||
<RecentPlayersCard data={data?.players}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
</ExtendableRow>
|
||||
<ExtendableRow id={'row-network-overview-1'}>
|
||||
<Col lg={4}>
|
||||
<ServerAsNumbersCard data={data?.numbers}/>
|
||||
</Col>
|
||||
<Col lg={8}>
|
||||
<ServerWeekComparisonCard data={data?.weeks}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import {Card, Col, Row} from "react-bootstrap";
|
||||
import {Card, Col} from "react-bootstrap";
|
||||
import {useMetadata} from "../../hooks/metadataHook";
|
||||
import CardHeader from "../../components/cards/CardHeader";
|
||||
import {faServer} from "@fortawesome/free-solid-svg-icons";
|
||||
@ -12,6 +12,7 @@ import PerformanceAsNumbersCard from "../../components/cards/server/tables/Perfo
|
||||
import {useNavigation} from "../../hooks/navigationHook";
|
||||
import {mapPerformanceDataToSeries} from "../../util/graphs";
|
||||
import PerformanceGraphsCard from "../../components/cards/network/PerformanceGraphsCard";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const NetworkPerformance = () => {
|
||||
const {t} = useTranslation();
|
||||
@ -87,13 +88,13 @@ const NetworkPerformance = () => {
|
||||
const isUpToDate = visualizedServers.every((s, i) => s === selectedOptions[i]);
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className={"network_performance"}>
|
||||
<Row>
|
||||
<section className={"network-performance"}>
|
||||
<ExtendableRow id={'row-network-performance-0'}>
|
||||
<Col>
|
||||
<PerformanceGraphsCard data={performanceData}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
</ExtendableRow>
|
||||
<ExtendableRow id={'row-network-performance-1'}>
|
||||
<Col md={8}>
|
||||
<PerformanceAsNumbersCard data={performanceData?.overview?.numbers}/>
|
||||
</Col>
|
||||
@ -108,7 +109,7 @@ const NetworkPerformance = () => {
|
||||
</button>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Col, Row} from "react-bootstrap";
|
||||
import {Col} from "react-bootstrap";
|
||||
import React from "react";
|
||||
import PlayerbaseDevelopmentCard from "../../components/cards/server/graphs/PlayerbaseDevelopmentCard";
|
||||
import CurrentPlayerbaseCard from "../../components/cards/server/graphs/CurrentPlayerbaseCard";
|
||||
@ -8,22 +8,23 @@ import PlayerbaseTrendsCard from "../../components/cards/server/tables/Playerbas
|
||||
import PlayerbaseInsightsCard from "../../components/cards/server/insights/PlayerbaseInsightsCard";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import {fetchNetworkPlayerbaseOverview} from "../../service/networkService";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const NetworkPlayerbaseOverview = () => {
|
||||
const {data, loadingError} = useDataRequest(fetchNetworkPlayerbaseOverview, []);
|
||||
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className="network_playerbase">
|
||||
<Row>
|
||||
<section className="network-playerbase">
|
||||
<ExtendableRow id={'row-network-playerbase-0'}>
|
||||
<Col lg={8}>
|
||||
<PlayerbaseDevelopmentCard identifier={undefined}/>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<CurrentPlayerbaseCard identifier={undefined}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
</ExtendableRow>
|
||||
<ExtendableRow id={'row-network-playerbase-1'}>
|
||||
{loadingError && <ErrorViewCard error={loadingError}/>}
|
||||
{!loadingError && <>
|
||||
<Col lg={8}>
|
||||
@ -33,7 +34,7 @@ const NetworkPlayerbaseOverview = () => {
|
||||
<PlayerbaseInsightsCard data={data?.insights}/>
|
||||
</Col>
|
||||
</>}
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -1,11 +1,12 @@
|
||||
import React, {useState} from 'react';
|
||||
import {Col, Row} from "react-bootstrap";
|
||||
import {Col} from "react-bootstrap";
|
||||
import {useDataRequest} from "../../hooks/dataFetchHook";
|
||||
import {fetchServersOverview} from "../../service/networkService";
|
||||
import ErrorView from "../ErrorView";
|
||||
import ServersTableCard from "../../components/cards/network/ServersTableCard";
|
||||
import QuickViewGraphCard from "../../components/cards/network/QuickViewGraphCard";
|
||||
import QuickViewDataCard from "../../components/cards/network/QuickViewDataCard";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const NetworkServers = () => {
|
||||
const [selectedServer, setSelectedServer] = useState(0);
|
||||
@ -17,7 +18,7 @@ const NetworkServers = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<ExtendableRow id={'row-network-servers-0'}>
|
||||
<Col md={6}>
|
||||
<ServersTableCard loaded={Boolean(data)} servers={data?.servers || []}
|
||||
onSelect={(index) => setSelectedServer(index)}/>
|
||||
@ -26,7 +27,7 @@ const NetworkServers = () => {
|
||||
{data?.servers.length && <QuickViewGraphCard server={data.servers[selectedServer]}/>}
|
||||
{data?.servers.length && <QuickViewDataCard server={data.servers[selectedServer]}/>}
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -1,15 +1,16 @@
|
||||
import {Col, Row} from "react-bootstrap";
|
||||
import {Col} from "react-bootstrap";
|
||||
import React from "react";
|
||||
import ServerRecentSessionsCard from "../../components/cards/server/tables/ServerRecentSessionsCard";
|
||||
import SessionInsightsCard from "../../components/cards/server/insights/SessionInsightsCard";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import ServerPieCard from "../../components/cards/common/ServerPieCard";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const NetworkSessions = () => {
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className="server_sessions">
|
||||
<Row>
|
||||
<section className="network-sessions">
|
||||
<ExtendableRow id={'row-network-sessions-0'}>
|
||||
<Col lg={8}>
|
||||
<ServerRecentSessionsCard identifier={undefined}/>
|
||||
</Col>
|
||||
@ -17,7 +18,7 @@ const NetworkSessions = () => {
|
||||
<ServerPieCard/>
|
||||
<SessionInsightsCard identifier={undefined}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import {Card, Col, Row} from "react-bootstrap";
|
||||
import {Card, Col} from "react-bootstrap";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faAddressBook, faCalendar, faCalendarCheck, faClock} from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
@ -29,6 +29,8 @@ import {useTranslation} from "react-i18next";
|
||||
import NicknamesCard from "../../components/cards/player/NicknamesCard";
|
||||
import {TableRow} from "../../components/table/TableRow";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import ExtendableCardBody from "../../components/layout/extension/ExtendableCardBody";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const PlayerOverviewCard = ({player}) => {
|
||||
const {t} = useTranslation();
|
||||
@ -42,8 +44,8 @@ const PlayerOverviewCard = ({player}) => {
|
||||
<Fa icon={faAddressBook}/> {player.info.name}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<Row>
|
||||
<ExtendableCardBody id={'card-body-player-overview-card'}>
|
||||
<ExtendableRow id={'row-player-overview-card-0'}>
|
||||
<Col sm={4}>
|
||||
<p>
|
||||
<Fa icon={faCircle} className={player.info.online ? "col-green" : "col-red"}/>
|
||||
@ -67,9 +69,9 @@ const PlayerOverviewCard = ({player}) => {
|
||||
className="col-green"/> {t('html.label.mobKills')}: {player.info.mob_kill_count}</p>
|
||||
<p><Fa icon={faSkull}/> {t('html.label.deaths')}: {player.info.death_count}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
<hr/>
|
||||
<Row>
|
||||
<ExtendableRow id={'row-player-overview-card-1'}>
|
||||
<Col lg={6}>
|
||||
<Datapoint
|
||||
icon={faClock} color="green"
|
||||
@ -136,8 +138,8 @@ const PlayerOverviewCard = ({player}) => {
|
||||
name={t('html.label.lastSeen')} value={player.info.last_seen} boldTitle
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card.Body>
|
||||
</ExtendableRow>
|
||||
</ExtendableCardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -222,8 +224,8 @@ const PlayerOverview = () => {
|
||||
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className="player_overview">
|
||||
<Row>
|
||||
<section className="player-overview">
|
||||
<ExtendableRow id={'row-player-overview-0'}>
|
||||
<Col lg={6}>
|
||||
<PlayerOverviewCard player={player}/>
|
||||
<NicknamesCard player={player}/>
|
||||
@ -233,7 +235,7 @@ const PlayerOverview = () => {
|
||||
<PunchCardCard player={player}/>
|
||||
<OnlineActivityCard player={player}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import {Card, Col, Row} from "react-bootstrap";
|
||||
import {Card, Col} from "react-bootstrap";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faLifeRing} from "@fortawesome/free-regular-svg-icons";
|
||||
import {faKhanda, faSkull} from "@fortawesome/free-solid-svg-icons";
|
||||
@ -10,6 +10,7 @@ import {useTranslation} from "react-i18next";
|
||||
import PvpPveAsNumbersCard from "../../components/cards/player/PvpPveAsNumbersCard";
|
||||
import PvpKillsTableCard from "../../components/cards/common/PvpKillsTableCard";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const InsightsCard = ({player}) => {
|
||||
const {t} = useTranslation();
|
||||
@ -50,23 +51,23 @@ const PlayerPvpPve = () => {
|
||||
const {player} = usePlayer();
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className="player_pvp_pve">
|
||||
<Row>
|
||||
<section className="player-pvp-pve">
|
||||
<ExtendableRow id={'row-player-pvp-pve-0'}>
|
||||
<Col lg={8}>
|
||||
<PvpPveAsNumbersCard player={player}/>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<InsightsCard player={player}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
</ExtendableRow>
|
||||
<ExtendableRow id={'row-player-pvp-pve-1'}>
|
||||
<Col lg={6}>
|
||||
<PvpKillsTableCard player_kills={player.player_kills}/>
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
<PvpDeathsTableCard player={player}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import {Card, Col, Row} from "react-bootstrap";
|
||||
import {Card, Col} from "react-bootstrap";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faHandPointer} from "@fortawesome/free-regular-svg-icons";
|
||||
import Scrollable from "../../components/Scrollable";
|
||||
@ -10,6 +10,7 @@ import {usePlayer} from "../layout/PlayerPage";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import PlayerPingGraph from "../../components/graphs/PlayerPingGraph";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const PingGraphCard = ({player}) => {
|
||||
const {t} = useTranslation();
|
||||
@ -70,20 +71,20 @@ const PlayerServers = () => {
|
||||
const {player} = usePlayer();
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className="player_sessions">
|
||||
<Row>
|
||||
<section className="player-servers">
|
||||
<ExtendableRow id={'row-player-servers-0'}>
|
||||
<Col lg={12}>
|
||||
<PingGraphCard player={player}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
</ExtendableRow>
|
||||
<ExtendableRow id={'row-player-servers-1'}>
|
||||
<Col lg={8}>
|
||||
<ServersCard player={player}/>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<ServerPieCard player={player}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import {Card, Col, Row} from "react-bootstrap";
|
||||
import {Card, Col} from "react-bootstrap";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faCalendarAlt} from "@fortawesome/free-regular-svg-icons";
|
||||
import PlayerSessionCalendar from "../../components/calendar/PlayerSessionCalendar";
|
||||
@ -8,6 +8,7 @@ import {useTranslation} from "react-i18next";
|
||||
import PlayerWorldPieCard from "../../components/cards/player/PlayerWorldPieCard";
|
||||
import PlayerRecentSessionsCard from "../../components/cards/player/PlayerRecentSessionsCard";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const SessionCalendarCard = ({player}) => {
|
||||
const {t} = useTranslation();
|
||||
@ -27,8 +28,8 @@ const PlayerSessions = () => {
|
||||
const {player} = usePlayer();
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className="player_sessions">
|
||||
<Row>
|
||||
<section className="player-sessions">
|
||||
<ExtendableRow id={'row-player-sessions-0'}>
|
||||
<Col lg={8}>
|
||||
<SessionCalendarCard player={player}/>
|
||||
<PlayerRecentSessionsCard player={player}/>
|
||||
@ -36,7 +37,7 @@ const PlayerSessions = () => {
|
||||
<Col lg={4}>
|
||||
<PlayerWorldPieCard player={player}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -2,10 +2,11 @@ import React from 'react';
|
||||
import {useDataRequest} from "../../hooks/dataFetchHook";
|
||||
import {fetchPlayers} from "../../service/serverService";
|
||||
import ErrorView from "../ErrorView";
|
||||
import {Col, Row} from "react-bootstrap";
|
||||
import {Col} from "react-bootstrap";
|
||||
import PlayerListCard from "../../components/cards/common/PlayerListCard";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import {CardLoader} from "../../components/navigation/Loader";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const AllPlayers = () => {
|
||||
const {data, loadingError} = useDataRequest(fetchPlayers, [null]);
|
||||
@ -14,11 +15,11 @@ const AllPlayers = () => {
|
||||
|
||||
return (
|
||||
<LoadIn>
|
||||
<Row>
|
||||
<ExtendableRow id={'row-player-list-0'}>
|
||||
<Col md={12}>
|
||||
{data ? <PlayerListCard data={data}/> : <CardLoader/>}
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</LoadIn>
|
||||
)
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import {Col, Row} from "react-bootstrap";
|
||||
import {Col} from "react-bootstrap";
|
||||
import OnlineActivityGraphsCard from "../../components/cards/server/graphs/OnlineActivityGraphsCard";
|
||||
import OnlineActivityAsNumbersCard from "../../components/cards/server/tables/OnlineActivityAsNumbersCard";
|
||||
import {useParams} from "react-router-dom";
|
||||
@ -8,6 +8,7 @@ import {fetchOnlineActivityOverview} from "../../service/serverService";
|
||||
import ErrorView from "../ErrorView";
|
||||
import OnlineActivityInsightsCard from "../../components/cards/server/insights/OnlineActivityInsightsCard";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const OnlineActivity = () => {
|
||||
const {identifier} = useParams();
|
||||
@ -18,20 +19,20 @@ const OnlineActivity = () => {
|
||||
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className="server_online_activity_overview">
|
||||
<Row>
|
||||
<section className="server-online-activity-overview">
|
||||
<ExtendableRow id={'row-server-online-activity-overview-0'}>
|
||||
<Col lg={12}>
|
||||
<OnlineActivityGraphsCard/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
</ExtendableRow>
|
||||
<ExtendableRow id={'row-server-online-activity-overview-1'}>
|
||||
<Col lg={8}>
|
||||
<OnlineActivityAsNumbersCard data={data?.numbers}/>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<OnlineActivityInsightsCard data={data?.insights}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Col, Row} from "react-bootstrap";
|
||||
import {Col} from "react-bootstrap";
|
||||
import React from "react";
|
||||
import PlayerbaseDevelopmentCard from "../../components/cards/server/graphs/PlayerbaseDevelopmentCard";
|
||||
import CurrentPlayerbaseCard from "../../components/cards/server/graphs/CurrentPlayerbaseCard";
|
||||
@ -9,6 +9,7 @@ import {ErrorViewCard} from "../ErrorView";
|
||||
import PlayerbaseTrendsCard from "../../components/cards/server/tables/PlayerbaseTrendsCard";
|
||||
import PlayerbaseInsightsCard from "../../components/cards/server/insights/PlayerbaseInsightsCard";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const PlayerbaseOverview = () => {
|
||||
const {identifier} = useParams();
|
||||
@ -17,16 +18,16 @@ const PlayerbaseOverview = () => {
|
||||
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className="server_playerbase">
|
||||
<Row>
|
||||
<section className="server-playerbase">
|
||||
<ExtendableRow id={'row-server-playerbase-0'}>
|
||||
<Col lg={8}>
|
||||
<PlayerbaseDevelopmentCard identifier={identifier}/>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<CurrentPlayerbaseCard identifier={identifier}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
</ExtendableRow>
|
||||
<ExtendableRow id={'row-server-playerbase-1'}>
|
||||
{loadingError && <ErrorViewCard error={loadingError}/>}
|
||||
{!loadingError && <>
|
||||
<Col lg={8}>
|
||||
@ -36,7 +37,7 @@ const PlayerbaseOverview = () => {
|
||||
<PlayerbaseInsightsCard data={data?.insights}/>
|
||||
</Col>
|
||||
</>}
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -11,7 +11,7 @@ const ServerGeolocations = () => {
|
||||
const {pingData, pingLoadingError} = useDataRequest(fetchPingTable, [identifier]);
|
||||
|
||||
return (
|
||||
<Geolocations className={"server_geolocations"}
|
||||
<Geolocations className={"server-geolocations"}
|
||||
geolocationData={data} geolocationError={loadingError}
|
||||
pingData={pingData} pingError={pingLoadingError}
|
||||
/>
|
||||
|
@ -1,23 +1,24 @@
|
||||
import React from 'react';
|
||||
import {Col, Row} from "react-bootstrap";
|
||||
import {Col} from "react-bootstrap";
|
||||
import JoinAddressGroupCard from "../../components/cards/server/graphs/JoinAddressGroupCard";
|
||||
import JoinAddressGraphCard from "../../components/cards/server/graphs/JoinAddressGraphCard";
|
||||
import {useParams} from "react-router-dom";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const ServerJoinAddresses = () => {
|
||||
const {identifier} = useParams();
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className={"server_join_addresses"}>
|
||||
<Row>
|
||||
<section className={"server-join-addresses"}>
|
||||
<ExtendableRow id={'row-server-join-addresses-0'}>
|
||||
<Col lg={8}>
|
||||
<JoinAddressGraphCard identifier={identifier}/>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<JoinAddressGroupCard identifier={identifier}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import {Card, Col, Row} from "react-bootstrap";
|
||||
import {Card, Col} from "react-bootstrap";
|
||||
import {faExclamationCircle, faPowerOff, faTachometerAlt, faUser, faUsers} from "@fortawesome/free-solid-svg-icons";
|
||||
import Datapoint from "../../components/Datapoint";
|
||||
import {useTranslation} from "react-i18next";
|
||||
@ -13,6 +13,7 @@ import ServerAsNumbersCard from "../../components/cards/server/values/ServerAsNu
|
||||
import ServerWeekComparisonCard from "../../components/cards/server/tables/ServerWeekComparisonCard";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import {CardLoader} from "../../components/navigation/Loader";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const Last7DaysCard = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
@ -68,23 +69,23 @@ const ServerOverview = () => {
|
||||
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className="server_overview">
|
||||
<Row>
|
||||
<section className="server-overview">
|
||||
<ExtendableRow id={'row-server-overview-0'}>
|
||||
<Col lg={9}>
|
||||
<OnlineActivityCard/>
|
||||
</Col>
|
||||
<Col lg={3}>
|
||||
<Last7DaysCard data={data?.last_7_days}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
</ExtendableRow>
|
||||
<ExtendableRow id={'row-server-overview-1'}>
|
||||
<Col lg={4}>
|
||||
<ServerAsNumbersCard data={data?.numbers}/>
|
||||
</Col>
|
||||
<Col lg={8}>
|
||||
<ServerWeekComparisonCard data={data?.weeks}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import {Col, Row} from "react-bootstrap";
|
||||
import {Col} from "react-bootstrap";
|
||||
import {useParams} from "react-router-dom";
|
||||
import {useDataRequest} from "../../hooks/dataFetchHook";
|
||||
import {fetchPerformanceOverview} from "../../service/serverService";
|
||||
@ -8,6 +8,7 @@ import PerformanceGraphsCard from "../../components/cards/server/graphs/Performa
|
||||
import PerformanceInsightsCard from "../../components/cards/server/insights/PerformanceInsightsCard";
|
||||
import {ErrorViewCard} from "../ErrorView";
|
||||
import PerformanceAsNumbersCard from "../../components/cards/server/tables/PerformanceAsNumbersCard";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const ServerPerformance = () => {
|
||||
const {identifier} = useParams();
|
||||
@ -16,13 +17,13 @@ const ServerPerformance = () => {
|
||||
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className="server_performance">
|
||||
<Row>
|
||||
<section className="server-performance">
|
||||
<ExtendableRow id={'row-server-performance-0'}>
|
||||
<Col lg={12}>
|
||||
<PerformanceGraphsCard/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
</ExtendableRow>
|
||||
<ExtendableRow id={'row-server-performance-1'}>
|
||||
<Col lg={8}>
|
||||
{loadingError ? <ErrorViewCard error={loadingError}/> :
|
||||
<PerformanceAsNumbersCard data={data?.numbers}/>}
|
||||
@ -31,7 +32,7 @@ const ServerPerformance = () => {
|
||||
{loadingError ? <ErrorViewCard error={loadingError}/> :
|
||||
<PerformanceInsightsCard data={data?.insights}/>}
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -3,9 +3,10 @@ import {useDataRequest} from "../../hooks/dataFetchHook";
|
||||
import {useParams} from "react-router-dom";
|
||||
import {fetchPlayers} from "../../service/serverService";
|
||||
import ErrorView from "../ErrorView";
|
||||
import {Col, Row} from "react-bootstrap";
|
||||
import {Col} from "react-bootstrap";
|
||||
import PlayerListCard from "../../components/cards/common/PlayerListCard";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const ServerPlayers = () => {
|
||||
const {identifier} = useParams();
|
||||
@ -16,12 +17,12 @@ const ServerPlayers = () => {
|
||||
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className="server_players">
|
||||
<Row>
|
||||
<section className="server-players">
|
||||
<ExtendableRow id={'row-server-players-0'}>
|
||||
<Col md={12}>
|
||||
<PlayerListCard data={data}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import PvpPveAsNumbersCard from "../../components/cards/server/tables/PvpPveAsNumbersCard";
|
||||
import {Col, Row} from "react-bootstrap";
|
||||
import {Col} from "react-bootstrap";
|
||||
import PvpKillsTableCard from "../../components/cards/common/PvpKillsTableCard";
|
||||
import PvpPveInsightsCard from "../../components/cards/server/insights/PvpPveInsightsCard";
|
||||
import {useParams} from "react-router-dom";
|
||||
@ -8,6 +8,7 @@ import {useDataRequest} from "../../hooks/dataFetchHook";
|
||||
import {fetchKills, fetchPvpPve} from "../../service/serverService";
|
||||
import ErrorView from "../ErrorView";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const ServerPvpPve = () => {
|
||||
const {identifier} = useParams();
|
||||
@ -20,20 +21,20 @@ const ServerPvpPve = () => {
|
||||
|
||||
return (
|
||||
<LoadIn show={data && killsData}>
|
||||
<section className="server_pvp_pve">
|
||||
<Row>
|
||||
<section className="server-pvp-pve">
|
||||
<ExtendableRow id={'row-server-pvp-pve-0'}>
|
||||
<Col lg={8}>
|
||||
<PvpPveAsNumbersCard kill_data={data?.numbers}/>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<PvpPveInsightsCard data={data?.insights}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
</ExtendableRow>
|
||||
<ExtendableRow id={'row-server-pvp-pve-1'}>
|
||||
<Col lg={8}>
|
||||
<PvpKillsTableCard player_kills={killsData?.player_kills}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
@ -1,17 +1,18 @@
|
||||
import {Col, Row} from "react-bootstrap";
|
||||
import {Col} from "react-bootstrap";
|
||||
import React from "react";
|
||||
import ServerWorldPieCard from "../../components/cards/server/graphs/ServerWorldPieCard";
|
||||
import ServerRecentSessionsCard from "../../components/cards/server/tables/ServerRecentSessionsCard";
|
||||
import SessionInsightsCard from "../../components/cards/server/insights/SessionInsightsCard";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import {useParams} from "react-router-dom";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
|
||||
const ServerSessions = () => {
|
||||
const {identifier} = useParams();
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className="server_sessions">
|
||||
<Row>
|
||||
<section className="server-sessions">
|
||||
<ExtendableRow id={'row-server-sessions-0'}>
|
||||
<Col lg={8}>
|
||||
<ServerRecentSessionsCard identifier={identifier}/>
|
||||
</Col>
|
||||
@ -19,7 +20,7 @@ const ServerSessions = () => {
|
||||
<ServerWorldPieCard/>
|
||||
<SessionInsightsCard identifier={identifier}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user