mirror of
https://github.com/Grasscutters/gcgm-plugin.git
synced 2024-11-25 15:32: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:
|
## Currently Planned Features:
|
||||||
- [x] Loading basic web page
|
- [x] Loading basic web page
|
||||||
- [x] Nice looking CSS
|
- [x] Nice looking CSS
|
||||||
- [ ] Websockets
|
- [x] Websockets (Although it is very basic)
|
||||||
- [ ] Widgets
|
- [ ] Widgets
|
||||||
- [ ] Server performance stats
|
- [x] Server performance stats
|
||||||
- [ ] See players registered to dispatch server
|
- [ ] See players registered to dispatch server
|
||||||
- [ ] See players currently online
|
- [ ] See players currently online
|
||||||
- [ ] Send mail to all players
|
- [ ] Send mail to all players
|
||||||
@ -17,8 +17,10 @@ The features listed are to achieve an MVP for the first release.
|
|||||||
|
|
||||||
## Important Notes:
|
## 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 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. \
|
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 guarenteed.**
|
**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
|
## Setup
|
||||||
### Download Plugin Jar
|
### 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.
|
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.
|
5. Navigate to your ``Grasscutter`` server, find the ``plugins`` folder and paste the ``gcgm-plugin.jar`` into it.
|
||||||
6. Start your server.
|
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] [GCGM] The './plugins/gcgm/www' folder does not exist.
|
||||||
[WARN] Please extract the contents of 'DefaultWebApp.zip' from within './plugins/gcgm' to './plugins/gcgm/www
|
[INFO] [GCGM] Copying 'DefaultWebApp.zip' to './plugins/GCGM'
|
||||||
[WARN] Your server will now exit to allow this process to be completed
|
[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
|
Your final plugins folder's directory structure should look similar to this
|
||||||
```
|
```
|
||||||
|
@ -19,10 +19,6 @@ plugins {
|
|||||||
sourceCompatibility = 17
|
sourceCompatibility = 17
|
||||||
targetCompatibility = 17
|
targetCompatibility = 17
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
// Use Maven Central for resolving dependencies.
|
// Use Maven Central for resolving dependencies.
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@ -39,16 +35,6 @@ repositories {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'lib', include: ['*.jar'])
|
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')
|
runtimeOnly project(':web-interface')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,14 +69,19 @@ jar {
|
|||||||
|
|
||||||
from {
|
from {
|
||||||
configurations.runtimeClasspath.collect {
|
configurations.runtimeClasspath.collect {
|
||||||
if(it.name.equalsIgnoreCase("zip4j-2.10.0.jar")) {
|
if(!it.name.contains("grasscutter")) {
|
||||||
println ('Packaging ' + it.name)
|
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(".")
|
destinationDir = file(".")
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,36 @@
|
|||||||
package com.benj4.gcgm;
|
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.Grasscutter;
|
||||||
import emu.grasscutter.plugin.Plugin;
|
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 emu.grasscutter.utils.Utils;
|
||||||
import express.Express;
|
import express.Express;
|
||||||
import io.javalin.http.staticfiles.Location;
|
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.File;
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.StandardCopyOption;
|
|
||||||
|
|
||||||
public class GCGMPlugin extends Plugin {
|
public class GCGMPlugin extends Plugin {
|
||||||
|
|
||||||
File webData;
|
private static GCGMPlugin INSTANCE;
|
||||||
|
EventHandler<ServerTickEvent> serverTickEventHandler;
|
||||||
|
private static WebSocketServer webSocketServer;
|
||||||
|
private File webData;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoad() {
|
public void onLoad() {
|
||||||
File pluginDataDir = getDataFolder();
|
INSTANCE = this;
|
||||||
webData = new File(Utils.toFilePath(getDataFolder().getPath() + "/www"));
|
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");
|
String zipFileLoc = Utils.toFilePath(getDataFolder().getPath() + "/DefaultWebApp.zip");
|
||||||
|
|
||||||
if(!pluginDataDir.exists() && !pluginDataDir.mkdirs()) {
|
if(!pluginDataDir.exists() && !pluginDataDir.mkdirs()) {
|
||||||
@ -30,35 +40,14 @@ public class GCGMPlugin extends Plugin {
|
|||||||
|
|
||||||
if(!webData.exists()) {
|
if(!webData.exists()) {
|
||||||
Grasscutter.getLogger().warn("[GCGM] The './plugins/GCGM/www' folder does not exist.");
|
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
|
// Copy the the zip from resources to the plugin's data directory
|
||||||
if(!new File(zipFileLoc).exists()) {
|
if(!new File(zipFileLoc).exists()) {
|
||||||
Grasscutter.getLogger().info("[GCGM] Copying 'DefaultWebApp.zip' to './plugins/GCGM'");
|
if(GCGMUtils.CopyFile("DefaultWebApp.zip", zipFileLoc)) {
|
||||||
Files.copy(defaultWebAppZip, Paths.get(new File(zipFileLoc).toURI()), StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
}
|
|
||||||
|
|
||||||
Grasscutter.getLogger().warn("[GCGM] Please extract the contents of 'DefaultWebApp.zip' from within './plugins/GCGM' to './plugins/GCGM/www");
|
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 {
|
} else {
|
||||||
if(new File(zipFileLoc).exists()) {
|
Grasscutter.getLogger().error("[GCGM] GCGM cannot start due to setup errors.");
|
||||||
Grasscutter.getLogger().info("[GCGM] Note: You can now safely delete 'DefaultWebApp.zip' from within './plugins/GCGM'");
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,22 +56,33 @@ public class GCGMPlugin extends Plugin {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
|
if(webData.exists()) {
|
||||||
Express app = Grasscutter.getDispatchServer().getServer();
|
Express app = Grasscutter.getDispatchServer().getServer();
|
||||||
|
WebUtils.addStaticFiles(app, webData);
|
||||||
|
webSocketServer = new WebSocketServer();
|
||||||
|
webSocketServer.start(app);
|
||||||
|
|
||||||
app.raw().config.precompressStaticFiles = false;
|
Grasscutter.getPluginManager().registerListener(serverTickEventHandler);
|
||||||
app.raw().config.addStaticFiles("/gm", webData.getAbsolutePath(), Location.EXTERNAL);
|
|
||||||
app.raw().config.addSinglePageRoot("/gm", Utils.toFilePath(getDataFolder().getPath() + "/www/index.html"), Location.EXTERNAL);
|
|
||||||
|
|
||||||
Grasscutter.getLogger().info("[GCGM] GCGM Enabled");
|
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.getLogger().info("[GCGM] You can access your GM panel by navigating to " + GCGMUtils.GetDispatchAddress() + WebUtils.PAGE_ROOT);
|
||||||
(Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) +
|
} else {
|
||||||
":" + (Grasscutter.getConfig().getDispatchOptions().PublicPort != 0 ? Grasscutter.getConfig().getDispatchOptions().PublicPort : Grasscutter.getConfig().getDispatchOptions().Port) +
|
Grasscutter.getLogger().error("[GCGM] GCGM could not find the 'www' folder inside its plugin directory");
|
||||||
"/gm"
|
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
|
@Override
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
Grasscutter.getLogger().info("[GCGM] GCGM Disabled");
|
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/jest-dom": "^5.16.4",
|
||||||
"@testing-library/react": "^13.1.1",
|
"@testing-library/react": "^13.1.1",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"chart.js": "^3.7.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
|
"react-chartjs-2": "^4.1.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"react-use-websocket": "^3.0.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -5115,6 +5118,11 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/check-types": {
|
||||||
"version": "11.1.2",
|
"version": "11.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz",
|
||||||
@ -13321,6 +13329,15 @@
|
|||||||
"node": ">=14"
|
"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": {
|
"node_modules/react-dev-utils": {
|
||||||
"version": "12.0.1",
|
"version": "12.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
"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": {
|
"node_modules/readable-stream": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/charcodes/-/charcodes-0.2.0.tgz",
|
||||||
"integrity": "sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ=="
|
"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": {
|
"check-types": {
|
||||||
"version": "11.1.2",
|
"version": "11.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz",
|
||||||
@ -25652,6 +25683,12 @@
|
|||||||
"whatwg-fetch": "^3.6.2"
|
"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": {
|
"react-dev-utils": {
|
||||||
"version": "12.0.1",
|
"version": "12.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
"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"
|
"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": {
|
"readable-stream": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
"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/jest-dom": "^5.16.4",
|
||||||
"@testing-library/react": "^13.1.1",
|
"@testing-library/react": "^13.1.1",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"chart.js": "^3.7.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
|
"react-chartjs-2": "^4.1.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"react-use-websocket": "^3.0.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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';
|
import Dashboard from './Dashboard';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import gclogo from "../img/grasscutter-icon.png"
|
import gclogo from "../../../img/grasscutter-icon.png"
|
||||||
import NavigationButton from './NavigationButton'
|
import NavigationButton from './NavigationButton'
|
||||||
import SimpleButton from './SimpleButton'
|
import SimpleButton from './SimpleButton'
|
||||||
import { faArrowLeft, faCogs, faGamepad, faHome, faNetworkWired } from '@fortawesome/free-solid-svg-icons'
|
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 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'
|
import './Dashboard.css'
|
||||||
|
|
||||||
@ -8,6 +9,7 @@ export default class Dashboard extends Component {
|
|||||||
return (
|
return (
|
||||||
<div className='dashboard'>
|
<div className='dashboard'>
|
||||||
<Sidepanel />
|
<Sidepanel />
|
||||||
|
<StatsWidget />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user