diff --git a/README.md b/README.md index 725c7dc..17dd70c 100644 --- a/README.md +++ b/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.** +

THE ISSUE TRACKER IS NOT A SUPPORT FORM.

## 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 ``` diff --git a/gc-plugin/build.gradle b/gc-plugin/build.gradle index 6d50449..a62cfc0 100644 --- a/gc-plugin/build.gradle +++ b/gc-plugin/build.gradle @@ -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(".") } diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/GCGMPlugin.java b/gc-plugin/src/main/java/com/benj4/gcgm/GCGMPlugin.java index f5c8efa..d98b28b 100644 --- a/gc-plugin/src/main/java/com/benj4/gcgm/GCGMPlugin.java +++ b/gc-plugin/src/main/java/com/benj4/gcgm/GCGMPlugin.java @@ -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 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.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; + } } diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/handlers/ServerTickHandler.java b/gc-plugin/src/main/java/com/benj4/gcgm/handlers/ServerTickHandler.java new file mode 100644 index 0000000..bc04937 --- /dev/null +++ b/gc-plugin/src/main/java/com/benj4/gcgm/handlers/ServerTickHandler.java @@ -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 { + 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(); + } + } +} diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/WebSocketServer.java b/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/WebSocketServer.java new file mode 100644 index 0000000..61d6e5f --- /dev/null +++ b/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/WebSocketServer.java @@ -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 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); + }); + } +} diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/json/WSData.java b/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/json/WSData.java new file mode 100644 index 0000000..e36a8bd --- /dev/null +++ b/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/json/WSData.java @@ -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; + } +} diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/utils/GCGMUtils.java b/gc-plugin/src/main/java/com/benj4/gcgm/utils/GCGMUtils.java new file mode 100644 index 0000000..51e3740 --- /dev/null +++ b/gc-plugin/src/main/java/com/benj4/gcgm/utils/GCGMUtils.java @@ -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; + } + } +} diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/utils/web/WebUtils.java b/gc-plugin/src/main/java/com/benj4/gcgm/utils/web/WebUtils.java new file mode 100644 index 0000000..442a9a3 --- /dev/null +++ b/gc-plugin/src/main/java/com/benj4/gcgm/utils/web/WebUtils.java @@ -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); + } +} diff --git a/web-interface/package-lock.json b/web-interface/package-lock.json index 7a7e84b..1df991e 100644 --- a/web-interface/package-lock.json +++ b/web-interface/package-lock.json @@ -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", diff --git a/web-interface/package.json b/web-interface/package.json index 0d82e7f..fa48038 100644 --- a/web-interface/package.json +++ b/web-interface/package.json @@ -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": { diff --git a/web-interface/src/App.css b/web-interface/src/App.css deleted file mode 100644 index 74b5e05..0000000 --- a/web-interface/src/App.css +++ /dev/null @@ -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); - } -} diff --git a/web-interface/src/App.js b/web-interface/src/App.js index 4cbc81d..a2e6d61 100644 --- a/web-interface/src/App.js +++ b/web-interface/src/App.js @@ -1,4 +1,3 @@ -import './App.css'; import Dashboard from './Dashboard'; function App() { diff --git a/web-interface/src/Components/NavigationButton.js b/web-interface/src/Components/Dashboard/Sidepanel/NavigationButton.js similarity index 100% rename from web-interface/src/Components/NavigationButton.js rename to web-interface/src/Components/Dashboard/Sidepanel/NavigationButton.js diff --git a/web-interface/src/Components/Sidepanel.css b/web-interface/src/Components/Dashboard/Sidepanel/Sidepanel.css similarity index 100% rename from web-interface/src/Components/Sidepanel.css rename to web-interface/src/Components/Dashboard/Sidepanel/Sidepanel.css diff --git a/web-interface/src/Components/Sidepanel.js b/web-interface/src/Components/Dashboard/Sidepanel/Sidepanel.js similarity index 94% rename from web-interface/src/Components/Sidepanel.js rename to web-interface/src/Components/Dashboard/Sidepanel/Sidepanel.js index 1fe96c9..0572a31 100644 --- a/web-interface/src/Components/Sidepanel.js +++ b/web-interface/src/Components/Dashboard/Sidepanel/Sidepanel.js @@ -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' diff --git a/web-interface/src/Components/SimpleButton.js b/web-interface/src/Components/Dashboard/Sidepanel/SimpleButton.js similarity index 100% rename from web-interface/src/Components/SimpleButton.js rename to web-interface/src/Components/Dashboard/Sidepanel/SimpleButton.js diff --git a/web-interface/src/Components/Dashboard/StatsWidget.css b/web-interface/src/Components/Dashboard/StatsWidget.css new file mode 100644 index 0000000..9046264 --- /dev/null +++ b/web-interface/src/Components/Dashboard/StatsWidget.css @@ -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; +} \ No newline at end of file diff --git a/web-interface/src/Components/Dashboard/StatsWidget.js b/web-interface/src/Components/Dashboard/StatsWidget.js new file mode 100644 index 0000000..d6170c4 --- /dev/null +++ b/web-interface/src/Components/Dashboard/StatsWidget.js @@ -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 ( +
+

Server Performance (Ticks)

+ l.tick), + borderColor: CHART_COLORS.red, + fill: false, + cubicInterpolationMode: 'monotone', + tension: 0.4 + } + ] + }} + options={{ + y: { + suggestedMin: 995, + suggestedMax: 1005 + } + }}/> +
+ ); +} diff --git a/web-interface/src/Dashboard.js b/web-interface/src/Dashboard.js index 3bd5c7f..49663df 100644 --- a/web-interface/src/Dashboard.js +++ b/web-interface/src/Dashboard.js @@ -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 (
+
) }