mirror of
https://github.com/Grasscutters/gcgm-plugin.git
synced 2024-11-22 04:42:52 +08:00
Websockets, Performance Monitoring, Refactor
This commit is contained in:
parent
191704b39b
commit
e6a94436dc
28
README.md
28
README.md
@ -4,9 +4,9 @@ GCGM is the first [Grasscutter](https://github.com/Grasscutters/Grasscutter) plu
|
||||
## Currently Planned Features:
|
||||
- [x] Loading basic web page
|
||||
- [x] Nice looking CSS
|
||||
- [ ] Websockets
|
||||
- [x] Websockets (Although it is very basic)
|
||||
- [ ] Widgets
|
||||
- [ ] Server performance stats
|
||||
- [x] Server performance stats
|
||||
- [ ] See players registered to dispatch server
|
||||
- [ ] See players currently online
|
||||
- [ ] Send mail to all players
|
||||
@ -17,8 +17,10 @@ The features listed are to achieve an MVP for the first release.
|
||||
|
||||
## Important Notes:
|
||||
This plugin is made to run on the current [Development](https://github.com/Grasscutters/Grasscutter/tree/development) branch of Grasscutter. \
|
||||
This plugin is in very early development and the web dashboard only displays a side panel, so it is not really in a usable state. \
|
||||
**If you require support please ask on the [Grasscutter Discord](https://discord.gg/T5vZU6UyeG). However, support is not guarenteed.**
|
||||
This plugin is in very early development and the web dashboard only displays a side panel and a performance graph. \
|
||||
**If you require support please ask on the [Grasscutter Discord](https://discord.gg/T5vZU6UyeG). However, support is not guaranteed.** \
|
||||
**If you encounter any issues, please report them on the [issue tracker](https://github.com/Grasscutters/gcgm-plugin/issues). However, please search to see if anyone else has encountered your issue before. Any duplicate issues will be closed.**
|
||||
<h3 style="margin:0;padding:0;">THE ISSUE TRACKER IS NOT A SUPPORT FORM.</h3>
|
||||
|
||||
## Setup
|
||||
### Download Plugin Jar
|
||||
@ -31,13 +33,21 @@ Coming soon!
|
||||
4. Assuming the build succeeded, in your file explorer navigate to the ``gc-plugin`` folder, you should have a ``gcgm-plugin.jar`` file, copy it.
|
||||
5. Navigate to your ``Grasscutter`` server, find the ``plugins`` folder and paste the ``gcgm-plugin.jar`` into it.
|
||||
6. Start your server.
|
||||
7. Your server should then start and after a few seconds, you should be greated with these messages and the server will quit.
|
||||
7. Your server should then start and after a few seconds, you should be greated with these messages.
|
||||
```
|
||||
[WARN] The './plugins/gcgm/www' folder does not exist.
|
||||
[WARN] Please extract the contents of 'DefaultWebApp.zip' from within './plugins/gcgm' to './plugins/gcgm/www
|
||||
[WARN] Your server will now exit to allow this process to be completed
|
||||
[WARN] [GCGM] The './plugins/gcgm/www' folder does not exist.
|
||||
[INFO] [GCGM] Copying 'DefaultWebApp.zip' to './plugins/GCGM'
|
||||
[WARN] [GCGM] Please extract the contents of 'DefaultWebApp.zip' from within './plugins/GCGM' to './plugins/GCGM/www
|
||||
[WARN] [GCGM] GCGM has now loaded...
|
||||
...
|
||||
[ERROR] [GCGM] GCGM could not find the 'www' folder inside its plugin directory
|
||||
[ERROR] [GCGM] Please make sure a 'www' folder exists by extracting 'DefaultWebApp.zip' into a new 'www' or download a third-party dashboard
|
||||
[ERROR] [GCGM] GCGM could not be enabled
|
||||
|
||||
```
|
||||
7. Inside the ``plugins`` folder there now should be a new folder with the name of ``gcgm``, when you open the folder, there should be a ``.zip`` file called ``DefaultWebApp.zip``. Extract the contents of this zip into a folder called ``www``.
|
||||
8. Type ``stop`` to stop the server
|
||||
9. Inside the ``plugins`` folder there now should be a new folder with the name of ``gcgm``, when you open the folder, there should be file called ``DefaultWebApp.zip``. Extract the contents of this zip into a folder called ``www``.
|
||||
10. Start your server
|
||||
|
||||
Your final plugins folder's directory structure should look similar to this
|
||||
```
|
||||
|
@ -19,10 +19,6 @@ plugins {
|
||||
sourceCompatibility = 17
|
||||
targetCompatibility = 17
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
repositories {
|
||||
// Use Maven Central for resolving dependencies.
|
||||
mavenCentral()
|
||||
@ -39,16 +35,6 @@ repositories {
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'lib', include: ['*.jar'])
|
||||
|
||||
implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.8'
|
||||
|
||||
implementation group: 'dev.morphia.morphia', name: 'morphia-core', version: '2.2.6'
|
||||
|
||||
//implementation group: 'tech.xigam', name: 'grasscutter', version: '1.0.2-dev'
|
||||
|
||||
implementation 'io.jsonwebtoken:jjwt-api:0.11.3'
|
||||
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.3', 'io.jsonwebtoken:jjwt-gson:0.11.3'
|
||||
|
||||
implementation group: 'net.lingala.zip4j', name: 'zip4j', version: '2.10.0'
|
||||
runtimeOnly project(':web-interface')
|
||||
}
|
||||
|
||||
@ -83,14 +69,19 @@ jar {
|
||||
|
||||
from {
|
||||
configurations.runtimeClasspath.collect {
|
||||
if(it.name.equalsIgnoreCase("zip4j-2.10.0.jar")) {
|
||||
if(!it.name.contains("grasscutter")) {
|
||||
println ('Packaging ' + it.name)
|
||||
zipTree(it);
|
||||
it.isDirectory() ? it : zipTree(it)
|
||||
}
|
||||
}
|
||||
} {
|
||||
exclude('META-INF/LICENSE*')
|
||||
exclude("META-INF/versions/9/module-info.class")
|
||||
exclude("META-INF/NOTICE")
|
||||
}
|
||||
|
||||
//duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
|
||||
destinationDir = file(".")
|
||||
}
|
||||
|
@ -1,26 +1,36 @@
|
||||
package com.benj4.gcgm;
|
||||
|
||||
import com.benj4.gcgm.handlers.ServerTickHandler;
|
||||
import com.benj4.gcgm.server.websocket.WebSocketServer;
|
||||
import com.benj4.gcgm.utils.*;
|
||||
import com.benj4.gcgm.utils.web.WebUtils;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.plugin.Plugin;
|
||||
import emu.grasscutter.server.event.EventHandler;
|
||||
import emu.grasscutter.server.event.HandlerPriority;
|
||||
import emu.grasscutter.server.event.game.ServerTickEvent;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import express.Express;
|
||||
import io.javalin.http.staticfiles.Location;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.io.File;
|
||||
|
||||
public class GCGMPlugin extends Plugin {
|
||||
|
||||
File webData;
|
||||
private static GCGMPlugin INSTANCE;
|
||||
EventHandler<ServerTickEvent> serverTickEventHandler;
|
||||
private static WebSocketServer webSocketServer;
|
||||
private File webData;
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
File pluginDataDir = getDataFolder();
|
||||
INSTANCE = this;
|
||||
webData = new File(Utils.toFilePath(getDataFolder().getPath() + "/www"));
|
||||
|
||||
serverTickEventHandler = new EventHandler<ServerTickEvent>(ServerTickEvent.class);
|
||||
serverTickEventHandler.listener(new ServerTickHandler());
|
||||
serverTickEventHandler.priority(HandlerPriority.HIGH);
|
||||
|
||||
File pluginDataDir = getDataFolder();
|
||||
String zipFileLoc = Utils.toFilePath(getDataFolder().getPath() + "/DefaultWebApp.zip");
|
||||
|
||||
if(!pluginDataDir.exists() && !pluginDataDir.mkdirs()) {
|
||||
@ -30,35 +40,14 @@ public class GCGMPlugin extends Plugin {
|
||||
|
||||
if(!webData.exists()) {
|
||||
Grasscutter.getLogger().warn("[GCGM] The './plugins/GCGM/www' folder does not exist.");
|
||||
|
||||
// Get the ZIP
|
||||
URL url = null;
|
||||
try {
|
||||
url = new File(Utils.toFilePath(Grasscutter.getConfig().PLUGINS_FOLDER + "/gcgm-plugin.jar")).toURI().toURL();
|
||||
} catch (MalformedURLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
//URLClassLoader loader = new URLClassLoader(new URL[]{url});
|
||||
|
||||
InputStream defaultWebAppZip = getResource("DefaultWebApp.zip");
|
||||
try {
|
||||
// Copy the the zip from resources to the plugin's data directory
|
||||
if(!new File(zipFileLoc).exists()) {
|
||||
Grasscutter.getLogger().info("[GCGM] Copying 'DefaultWebApp.zip' to './plugins/GCGM'");
|
||||
Files.copy(defaultWebAppZip, Paths.get(new File(zipFileLoc).toURI()), StandardCopyOption.REPLACE_EXISTING);
|
||||
// Copy the the zip from resources to the plugin's data directory
|
||||
if(!new File(zipFileLoc).exists()) {
|
||||
if(GCGMUtils.CopyFile("DefaultWebApp.zip", zipFileLoc)) {
|
||||
Grasscutter.getLogger().warn("[GCGM] Please extract the contents of 'DefaultWebApp.zip' from within './plugins/GCGM' to './plugins/GCGM/www");
|
||||
} else {
|
||||
Grasscutter.getLogger().error("[GCGM] GCGM cannot start due to setup errors.");
|
||||
return;
|
||||
}
|
||||
|
||||
Grasscutter.getLogger().warn("[GCGM] Please extract the contents of 'DefaultWebApp.zip' from within './plugins/GCGM' to './plugins/GCGM/www");
|
||||
Grasscutter.getLogger().warn("[GCGM] Your server will now exit to allow this process to be completed");
|
||||
|
||||
System.exit(0);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
if(new File(zipFileLoc).exists()) {
|
||||
Grasscutter.getLogger().info("[GCGM] Note: You can now safely delete 'DefaultWebApp.zip' from within './plugins/GCGM'");
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,22 +56,33 @@ public class GCGMPlugin extends Plugin {
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
Express app = Grasscutter.getDispatchServer().getServer();
|
||||
if(webData.exists()) {
|
||||
Express app = Grasscutter.getDispatchServer().getServer();
|
||||
WebUtils.addStaticFiles(app, webData);
|
||||
webSocketServer = new WebSocketServer();
|
||||
webSocketServer.start(app);
|
||||
|
||||
app.raw().config.precompressStaticFiles = false;
|
||||
app.raw().config.addStaticFiles("/gm", webData.getAbsolutePath(), Location.EXTERNAL);
|
||||
app.raw().config.addSinglePageRoot("/gm", Utils.toFilePath(getDataFolder().getPath() + "/www/index.html"), Location.EXTERNAL);
|
||||
Grasscutter.getPluginManager().registerListener(serverTickEventHandler);
|
||||
|
||||
Grasscutter.getLogger().info("[GCGM] GCGM Enabled");
|
||||
Grasscutter.getLogger().info("[GCGM] You can access your GM panel by navigating to http" + (Grasscutter.getConfig().getDispatchOptions().FrontHTTPS ? "s" : "") + "://" +
|
||||
(Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) +
|
||||
":" + (Grasscutter.getConfig().getDispatchOptions().PublicPort != 0 ? Grasscutter.getConfig().getDispatchOptions().PublicPort : Grasscutter.getConfig().getDispatchOptions().Port) +
|
||||
"/gm"
|
||||
);
|
||||
Grasscutter.getLogger().info("[GCGM] GCGM Enabled");
|
||||
Grasscutter.getLogger().info("[GCGM] You can access your GM panel by navigating to " + GCGMUtils.GetDispatchAddress() + WebUtils.PAGE_ROOT);
|
||||
} else {
|
||||
Grasscutter.getLogger().error("[GCGM] GCGM could not find the 'www' folder inside its plugin directory");
|
||||
Grasscutter.getLogger().error("[GCGM] Please make sure a 'www' folder exists by extracting 'DefaultWebApp.zip' into a new 'www' or download a third-party dashboard");
|
||||
Grasscutter.getLogger().error("[GCGM] GCGM could not be enabled");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
Grasscutter.getLogger().info("[GCGM] GCGM Disabled");
|
||||
}
|
||||
|
||||
public static GCGMPlugin GetInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static WebSocketServer getWebSocketServer() {
|
||||
return webSocketServer;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
package com.benj4.gcgm.handlers;
|
||||
|
||||
import com.benj4.gcgm.GCGMPlugin;
|
||||
import com.benj4.gcgm.server.websocket.json.WSData;
|
||||
import emu.grasscutter.server.event.game.ServerTickEvent;
|
||||
import emu.grasscutter.utils.EventConsumer;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public class ServerTickHandler implements EventConsumer<ServerTickEvent> {
|
||||
private static Instant lastTick;
|
||||
|
||||
@Override
|
||||
public void consume(ServerTickEvent serverTickEvent) {
|
||||
if(lastTick != null) {
|
||||
Instant now = Instant.now();
|
||||
long timeTaken = now.toEpochMilli() - lastTick.toEpochMilli();
|
||||
GCGMPlugin.getWebSocketServer().broadcast(new WSData("tick", timeTaken));
|
||||
lastTick = now;
|
||||
} else {
|
||||
lastTick = Instant.now();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package com.benj4.gcgm.server.websocket;
|
||||
|
||||
import com.benj4.gcgm.server.websocket.json.WSData;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import express.Express;
|
||||
import io.javalin.websocket.WsContext;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class WebSocketServer {
|
||||
|
||||
//SocketIOServer socketIOServer;
|
||||
private static Map<WsContext, String> userUsernameMap = new ConcurrentHashMap<>();
|
||||
|
||||
public void start(Express app) {
|
||||
app.ws("/gm", ws -> {
|
||||
ws.onConnect(ctx -> {
|
||||
String username = "Not logged in";
|
||||
userUsernameMap.put(ctx, username);
|
||||
Grasscutter.getLogger().info("[GCGM] User logged in to panel");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void broadcast(WSData data) {
|
||||
userUsernameMap.keySet().stream().filter(ctx -> ctx.session.isOpen()).forEach(session -> {
|
||||
session.send(data);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.benj4.gcgm.server.websocket.json;
|
||||
|
||||
public class WSData {
|
||||
public String eventName;
|
||||
public Object object;
|
||||
|
||||
public WSData(String eventName, Object object) {
|
||||
this.eventName = eventName;
|
||||
this.object = object;
|
||||
}
|
||||
}
|
31
gc-plugin/src/main/java/com/benj4/gcgm/utils/GCGMUtils.java
Normal file
31
gc-plugin/src/main/java/com/benj4/gcgm/utils/GCGMUtils.java
Normal file
@ -0,0 +1,31 @@
|
||||
package com.benj4.gcgm.utils;
|
||||
|
||||
import com.benj4.gcgm.GCGMPlugin;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
public class GCGMUtils {
|
||||
|
||||
public static String GetDispatchAddress() {
|
||||
return "http" + (Grasscutter.getConfig().getDispatchOptions().FrontHTTPS ? "s" : "") + "://" +
|
||||
(Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) +
|
||||
":" + (Grasscutter.getConfig().getDispatchOptions().PublicPort != 0 ? Grasscutter.getConfig().getDispatchOptions().PublicPort : Grasscutter.getConfig().getDispatchOptions().Port);
|
||||
}
|
||||
|
||||
public static boolean CopyFile(String resourceName, String copyLocation) {
|
||||
try {
|
||||
Grasscutter.getLogger().info("[GCGM] Copying 'DefaultWebApp.zip' to './plugins/GCGM'");
|
||||
Files.copy(GCGMPlugin.GetInstance().getResource(resourceName), Paths.get(new File(copyLocation).toURI()), StandardCopyOption.REPLACE_EXISTING);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Grasscutter.getLogger().error(String.format("[GCGM] An error occurred while trying to copy '%s' to '%s'", resourceName, copyLocation));
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.benj4.gcgm.utils.web;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import express.Express;
|
||||
import io.javalin.http.staticfiles.Location;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class WebUtils {
|
||||
|
||||
public static final String PAGE_ROOT = "/gm";
|
||||
|
||||
public static void addStaticFiles(Express app, File staticRoot) {
|
||||
app.raw().config.precompressStaticFiles = false; // MUST BE SET TO FALSE OR FILES SUCH AS IMAGES WILL APPEAR CORRUPTED
|
||||
app.raw().config.addStaticFiles(PAGE_ROOT, staticRoot.getAbsolutePath(), Location.EXTERNAL);
|
||||
app.raw().config.addSinglePageRoot(PAGE_ROOT, Utils.toFilePath(staticRoot.getPath() + "/index.html"), Location.EXTERNAL);
|
||||
}
|
||||
}
|
43
web-interface/package-lock.json
generated
43
web-interface/package-lock.json
generated
@ -14,10 +14,13 @@
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.1.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"chart.js": "^3.7.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"react": "^18.0.0",
|
||||
"react-chartjs-2": "^4.1.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-use-websocket": "^3.0.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
}
|
||||
},
|
||||
@ -5115,6 +5118,11 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.1.tgz",
|
||||
"integrity": "sha512-8knRegQLFnPQAheZV8MjxIXc5gQEfDFD897BJgv/klO/vtIyFFmgMXrNfgrXpbTr/XbTturxRgxIXx/Y+ASJBA=="
|
||||
},
|
||||
"node_modules/check-types": {
|
||||
"version": "11.1.2",
|
||||
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz",
|
||||
@ -13321,6 +13329,15 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/react-chartjs-2": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-4.1.0.tgz",
|
||||
"integrity": "sha512-AsUihxEp8Jm1oBhbEovE+w50m9PVNhz1sfwEIT4hZduRC0m14gHWHd0cUaxkFDb8HNkdMIGzsNlmVqKiOpU74g==",
|
||||
"peerDependencies": {
|
||||
"chart.js": "^3.5.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dev-utils": {
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
||||
@ -13540,6 +13557,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-use-websocket": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-3.0.0.tgz",
|
||||
"integrity": "sha512-BInlbhXYrODBPKIplDAmI0J1VPM+1KhCLN09o+dzgQ8qMyrYs4t5kEYmCrTqyRuMTmpahylHFZWQXpfYyDkqOw==",
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0",
|
||||
"react-dom": ">= 16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
@ -19858,6 +19884,11 @@
|
||||
"resolved": "https://registry.npmjs.org/charcodes/-/charcodes-0.2.0.tgz",
|
||||
"integrity": "sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ=="
|
||||
},
|
||||
"chart.js": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.1.tgz",
|
||||
"integrity": "sha512-8knRegQLFnPQAheZV8MjxIXc5gQEfDFD897BJgv/klO/vtIyFFmgMXrNfgrXpbTr/XbTturxRgxIXx/Y+ASJBA=="
|
||||
},
|
||||
"check-types": {
|
||||
"version": "11.1.2",
|
||||
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz",
|
||||
@ -25652,6 +25683,12 @@
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"react-chartjs-2": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-4.1.0.tgz",
|
||||
"integrity": "sha512-AsUihxEp8Jm1oBhbEovE+w50m9PVNhz1sfwEIT4hZduRC0m14gHWHd0cUaxkFDb8HNkdMIGzsNlmVqKiOpU74g==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-dev-utils": {
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
||||
@ -25817,6 +25854,12 @@
|
||||
"workbox-webpack-plugin": "^6.4.1"
|
||||
}
|
||||
},
|
||||
"react-use-websocket": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-3.0.0.tgz",
|
||||
"integrity": "sha512-BInlbhXYrODBPKIplDAmI0J1VPM+1KhCLN09o+dzgQ8qMyrYs4t5kEYmCrTqyRuMTmpahylHFZWQXpfYyDkqOw==",
|
||||
"requires": {}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
|
@ -9,10 +9,13 @@
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.1.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"chart.js": "^3.7.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"react": "^18.0.0",
|
||||
"react-chartjs-2": "^4.1.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-use-websocket": "^3.0.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
@ -1,38 +0,0 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import './App.css';
|
||||
import Dashboard from './Dashboard';
|
||||
|
||||
function App() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react'
|
||||
import gclogo from "../img/grasscutter-icon.png"
|
||||
import gclogo from "../../../img/grasscutter-icon.png"
|
||||
import NavigationButton from './NavigationButton'
|
||||
import SimpleButton from './SimpleButton'
|
||||
import { faArrowLeft, faCogs, faGamepad, faHome, faNetworkWired } from '@fortawesome/free-solid-svg-icons'
|
12
web-interface/src/Components/Dashboard/StatsWidget.css
Normal file
12
web-interface/src/Components/Dashboard/StatsWidget.css
Normal file
@ -0,0 +1,12 @@
|
||||
.graph {
|
||||
position: absolute;
|
||||
transform: translate(-50%, 0%);
|
||||
top: 6%;
|
||||
left: 50%;
|
||||
border-color: grey;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 8px;
|
||||
width: 800px;
|
||||
text-align: center;
|
||||
}
|
94
web-interface/src/Components/Dashboard/StatsWidget.js
Normal file
94
web-interface/src/Components/Dashboard/StatsWidget.js
Normal file
@ -0,0 +1,94 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
} from 'chart.js';
|
||||
import { useWebSocket, ReadyState } from "react-use-websocket/dist/lib/use-websocket";
|
||||
import { Line } from "react-chartjs-2";
|
||||
|
||||
import "./StatsWidget.css";
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
);
|
||||
|
||||
const CHART_COLORS = {
|
||||
red: 'rgb(255, 99, 132)',
|
||||
orange: 'rgb(255, 159, 64)',
|
||||
yellow: 'rgb(255, 205, 86)',
|
||||
green: 'rgb(75, 192, 192)',
|
||||
blue: 'rgb(54, 162, 235)',
|
||||
purple: 'rgb(153, 102, 255)',
|
||||
grey: 'rgb(201, 203, 207)'
|
||||
};
|
||||
|
||||
const TICK_COUNT = 50;
|
||||
const labels = [];
|
||||
for (let i = 0; i <= TICK_COUNT; ++i) {
|
||||
labels.push(i.toString());
|
||||
}
|
||||
|
||||
export default function StatsWidget() {
|
||||
const protocol = window.location.protocol === "https:" ? "wss" : "wss"
|
||||
const host = window.location.host;
|
||||
const port = window.location.port;
|
||||
|
||||
const { sendMessage, lastMessage, readyState } = useWebSocket(protocol + "://"+ host +":" + port + "/gm");
|
||||
const [graphHistory, setGraphHistory] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (lastMessage !== null) {
|
||||
const data = JSON.parse(lastMessage.data);
|
||||
var newGraph = graphHistory;
|
||||
newGraph.push({ time: new Date().getTime(), tick: data.object});
|
||||
if(newGraph.length > TICK_COUNT) {
|
||||
newGraph.splice(0, 1);
|
||||
}
|
||||
setGraphHistory(newGraph);
|
||||
}
|
||||
}, [lastMessage, setGraphHistory]);
|
||||
|
||||
/*const connectionStatus = {
|
||||
[ReadyState.CONNECTING]: "Connecting",
|
||||
[ReadyState.OPEN]: "Open",
|
||||
[ReadyState.CLOSING]: "Closing",
|
||||
[ReadyState.CLOSED]: "Closed",
|
||||
[ReadyState.UNINSTANTIATED]: "Uninstantiated",
|
||||
}[readyState];*/
|
||||
|
||||
return (
|
||||
<div className="graph">
|
||||
<p> Server Performance (Ticks) </p>
|
||||
<Line
|
||||
datasetIdKey="1"
|
||||
data={{
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: '',
|
||||
data: graphHistory.map((l) => l.tick),
|
||||
borderColor: CHART_COLORS.red,
|
||||
fill: false,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
tension: 0.4
|
||||
}
|
||||
]
|
||||
}}
|
||||
options={{
|
||||
y: {
|
||||
suggestedMin: 995,
|
||||
suggestedMax: 1005
|
||||
}
|
||||
}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import React, { Component } from 'react'
|
||||
import Sidepanel from './Components/Sidepanel'
|
||||
import Sidepanel from './Components/Dashboard/Sidepanel/Sidepanel'
|
||||
import StatsWidget from './Components/Dashboard/StatsWidget'
|
||||
|
||||
import './Dashboard.css'
|
||||
|
||||
@ -8,6 +9,7 @@ export default class Dashboard extends Component {
|
||||
return (
|
||||
<div className='dashboard'>
|
||||
<Sidepanel />
|
||||
<StatsWidget />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user