From 23efaafe1da77269c2f87379d7c8979ce87afb83 Mon Sep 17 00:00:00 2001 From: dunemask Date: Mon, 22 Jan 2024 01:01:12 +0000 Subject: [PATCH] [FEATURE] Quality of Life Improvements for Management (#7) Co-authored-by: Dunemask Reviewed-on: https://gitea.dunemask.dev/elysium/minecluster/pulls/7 --- Dockerfile | 3 +- lib/controllers/lifecycle-controller.js | 10 +- .../migrations/1_create_servers_table.sql | 1 + lib/database/queries/server-queries.js | 6 + lib/k8s/configs/extra-svc.yml | 23 ++ lib/k8s/k8s-server-control.js | 1 + lib/k8s/server-create.js | 59 +++- lib/k8s/server-delete.js | 5 + lib/k8s/server-files.js | 2 +- package-lock.json | 284 +++++++++++++++++- package.json | 1 + src/components/files/FilePreview.jsx | 43 ++- src/components/files/MineclusterFiles.jsx | 5 +- src/components/files/TextEditor.jsx | 21 ++ .../server-options/ExtraPortsOption.jsx | 41 +++ src/pages/CreateCoreOptions.jsx | 4 +- src/pages/Files.jsx | 7 +- src/util/queries.js | 2 +- 18 files changed, 479 insertions(+), 39 deletions(-) create mode 100644 lib/k8s/configs/extra-svc.yml create mode 100644 src/components/files/TextEditor.jsx create mode 100644 src/components/server-options/ExtraPortsOption.jsx diff --git a/Dockerfile b/Dockerfile index c80bac8..c578d05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,9 @@ RUN npm i COPY public public COPY dist dist COPY src src -COPY lib lib COPY index.html . COPY vite.config.js . RUN npm run build:react +# Copy Backend resources over +COPY lib lib CMD ["npm","start"] diff --git a/lib/controllers/lifecycle-controller.js b/lib/controllers/lifecycle-controller.js index 96073ed..57b8385 100644 --- a/lib/controllers/lifecycle-controller.js +++ b/lib/controllers/lifecycle-controller.js @@ -15,7 +15,7 @@ const dnsRegex = new RegExp( function payloadFilter(req, res) { const serverSpec = req.body; if (!serverSpec) return res.sendStatus(400); - const { name, host, version, serverType, memory } = serverSpec; + const { name, host, version, serverType, memory, extraPorts } = serverSpec; const { backupHost, backupBucket, backupId, backupKey, backupInterval } = serverSpec; if (!name) return res.status(400).send("Server name is required!"); @@ -24,6 +24,14 @@ function payloadFilter(req, res) { if (!version) return res.status(400).send("Server version is required!"); if (!serverType) return res.status(400).send("Server type is required!"); if (!memory) return res.status(400).send("Memory is required!"); + if ( + !!extraPorts && + (!Array.isArray(extraPorts) || + extraPorts.find((e) => typeof e !== "string" || e.length > 5)) + ) + return res + .status(400) + .send("Extra ports must be a list of strings with length of 5!"); // TODO: Impliment non creation time backups if ( !!backupHost || diff --git a/lib/database/migrations/1_create_servers_table.sql b/lib/database/migrations/1_create_servers_table.sql index 2f22691..3ef14ee 100644 --- a/lib/database/migrations/1_create_servers_table.sql +++ b/lib/database/migrations/1_create_servers_table.sql @@ -13,6 +13,7 @@ CREATE TABLE servers ( backup_id varchar(255) DEFAULT NULL, backup_key varchar(255) DEFAULT NULL, backup_interval varchar(255) DEFAULT NULL, + extra_ports varchar(7)[] DEFAULT NULL, CONSTRAINT unique_host UNIQUE(host) ); ALTER SEQUENCE servers_id_seq OWNED BY servers.id; \ No newline at end of file diff --git a/lib/database/queries/server-queries.js b/lib/database/queries/server-queries.js index 15bf2ac..93e84bd 100644 --- a/lib/database/queries/server-queries.js +++ b/lib/database/queries/server-queries.js @@ -16,6 +16,7 @@ export async function createServerEntry(serverSpec) { version, serverType: server_type, memory, + extraPorts: extra_ports, backupHost: backup_host, backupBucket: backup_bucket_path, backupId: backup_id, @@ -28,6 +29,7 @@ export async function createServerEntry(serverSpec) { version, server_type, memory, + extra_ports, backup_enabled: !!backup_interval, // We already verified the payload, so any backup key will work backup_host, backup_bucket_path, @@ -45,6 +47,7 @@ export async function createServerEntry(serverSpec) { version, server_type: serverType, memory, + extra_ports: extraPorts, backup_enabled: backupEnabled, backup_host: backupHost, backup_bucket_path: backupPath, @@ -61,6 +64,7 @@ export async function createServerEntry(serverSpec) { version, serverType, memory, + extraPorts, backupEnabled, backupHost, backupPath, @@ -94,6 +98,7 @@ export async function getServerEntry(serverId) { version, server_type: serverType, memory, + extra_ports: extraPorts, backup_enabled: backupEnabled, backup_host: backupHost, backup_bucket_path: backupPath, @@ -110,6 +115,7 @@ export async function getServerEntry(serverId) { version, serverType, memory, + extraPorts, backupEnabled, backupHost, backupPath, diff --git a/lib/k8s/configs/extra-svc.yml b/lib/k8s/configs/extra-svc.yml new file mode 100644 index 0000000..447a169 --- /dev/null +++ b/lib/k8s/configs/extra-svc.yml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + minecluster.dunemask.net/id: changeme-server-id + labels: + app: changeme-app + name: changeme-extra + namespace: changeme-namespace +spec: + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + # ports: Programatically generated + # - name: port-name + # port: 1234 + # protocol: TCP + # targetPort: port-name + selector: + app: changeme-app + sessionAffinity: None + type: ClusterIP diff --git a/lib/k8s/k8s-server-control.js b/lib/k8s/k8s-server-control.js index ec40b90..f82187a 100644 --- a/lib/k8s/k8s-server-control.js +++ b/lib/k8s/k8s-server-control.js @@ -78,6 +78,7 @@ export function getServerAssets(serverId) { backupSecret: secrets.find((s) => s.metadata.name.endsWith("-backup-secret"), ), + extraService: services.find((s) => s.metadata.name.endsWith("-extra")), }; for (var k in serverAssets) if (serverAssets[k]) return serverAssets; // If no assets exist, return nothing diff --git a/lib/k8s/server-create.js b/lib/k8s/server-create.js index c1c77ba..7578838 100644 --- a/lib/k8s/server-create.js +++ b/lib/k8s/server-create.js @@ -19,6 +19,37 @@ const namespace = process.env.MCL_SERVER_NAMESPACE; const loadYaml = (f) => yaml.load(fs.readFileSync(path.resolve(f), "utf8")); +function createExtraService(serverSpec) { + const { mclName, id, extraPorts } = serverSpec; + if (!extraPorts) return; + const serviceYaml = loadYaml("lib/k8s/configs/extra-svc.yml"); + serviceYaml.metadata.labels.app = `mcl-${mclName}-app`; + serviceYaml.metadata.name = `mcl-${mclName}-extra`; + serviceYaml.metadata.namespace = namespace; + serviceYaml.metadata.annotations["minecluster.dunemask.net/id"] = id; + serviceYaml.spec.selector.app = `mcl-${mclName}-app`; + // Port List: + const portList = extraPorts.map((p) => ({ + port: parseInt(p), + name: `mcl-extra-${p}`, + })); + const tcpPorts = portList.map(({ port, name }) => ({ + port, + name: `${name}-tcp`, + protocol: "TCP", + targetPort: port, + })); + const udpPorts = portList.map(({ port, name }) => ({ + port, + name: `${name}-udp`, + protocol: "UDP", + targetPort: port, + })); + + serviceYaml.spec.ports = [...tcpPorts, ...udpPorts]; + return serviceYaml; +} + function createBackupSecret(serverSpec) { if (!serverSpec.backupEnabled) return; // If backup not defined, don't create RCLONE secret const { mclName, id, backupId, backupKey, backupHost } = serverSpec; @@ -161,10 +192,26 @@ export default async function createServerResources(createSpec) { const serverDeploy = createServerDeploy(createSpec); const serverService = createServerService(createSpec); const rconService = createRconService(createSpec); - k8sCore.createNamespacedPersistentVolumeClaim(namespace, serverVolume); - if (!!backupSecret) k8sCore.createNamespacedSecret(namespace, backupSecret); - k8sCore.createNamespacedSecret(namespace, rconSecret); - k8sCore.createNamespacedService(namespace, serverService); - k8sCore.createNamespacedService(namespace, rconService); - k8sDeps.createNamespacedDeployment(namespace, serverDeploy); + const extraService = createExtraService(createSpec); + const serverResources = []; + serverResources.push( + k8sCore.createNamespacedPersistentVolumeClaim(namespace, serverVolume), + ); + if (!!extraService) + serverResources.push( + k8sCore.createNamespacedService(namespace, extraService), + ); + if (!!backupSecret) + serverResources.push( + k8sCore.createNamespacedSecret(namespace, backupSecret), + ); + serverResources.push(k8sCore.createNamespacedSecret(namespace, rconSecret)); + serverResources.push( + k8sCore.createNamespacedService(namespace, serverService), + ); + serverResources.push(k8sCore.createNamespacedService(namespace, rconService)); + serverResources.push( + k8sDeps.createNamespacedDeployment(namespace, serverDeploy), + ); + return await Promise.all(serverResources); } diff --git a/lib/k8s/server-delete.js b/lib/k8s/server-delete.js index c47b383..4ced830 100644 --- a/lib/k8s/server-delete.js +++ b/lib/k8s/server-delete.js @@ -51,6 +51,10 @@ export default async function deleteServerResources(serverSpec) { const deleteBackupSecret = deleteOnExist(server.backupSecret, (name) => k8sCore.deleteNamespacedSecret(name, namespace), ); + + const deleteExtraService = deleteOnExist(server.extraService, (name) => + k8sCore.deleteNamespacedService(name, namespace), + ); const deleteVolume = deleteOnExist(server.volume, (name) => k8sCore.deleteNamespacedPersistentVolumeClaim(name, namespace), ); @@ -59,6 +63,7 @@ export default async function deleteServerResources(serverSpec) { deleteService, deleteRconService, deleteRconSecret, + deleteExtraService, deleteBackupSecret, deleteVolume, ]).catch(deleteError); diff --git a/lib/k8s/server-files.js b/lib/k8s/server-files.js index 4f04f47..ce29a02 100644 --- a/lib/k8s/server-files.js +++ b/lib/k8s/server-files.js @@ -82,7 +82,7 @@ export async function uploadServerItem(serverSpec, file) { const { path } = serverSpec; pathSecurityCheck(path); await useServerFtp(serverSpec, async (c) => { - await c.uploadFrom(fileStream, `${path}/${file.originalname}`); + await c.uploadFrom(fileStream, path); }).catch(handleError); } diff --git a/package-lock.json b/package-lock.json index 63c29b0..a773352 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "prettier": "^3.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-quill": "^2.0.0", "react-router-dom": "^6.20.1", "react-toastify": "^9.1.3", "socket.io-client": "^4.7.2", @@ -3463,6 +3464,15 @@ "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", "dev": true }, + "node_modules/@types/quill": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz", + "integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==", + "dev": true, + "dependencies": { + "parchment": "^1.1.2" + } + }, "node_modules/@types/react": { "version": "17.0.73", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.73.tgz", @@ -4375,6 +4385,15 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/clsx": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", @@ -4712,6 +4731,26 @@ } } }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "dev": true, + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -4721,6 +4760,37 @@ "node": ">=0.10.0" } }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4942,6 +5012,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==", + "dev": true + }, "node_modules/events": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", @@ -5028,6 +5104,12 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", + "dev": true + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -5229,9 +5311,21 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/fuzzy-search": { "version": "3.2.1", @@ -5277,13 +5371,14 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5373,6 +5468,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -5389,6 +5485,29 @@ "node": ">=4" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", @@ -5419,6 +5538,17 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -5633,6 +5763,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -5691,6 +5836,22 @@ "node": ">=0.12.0" } }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-typed-array": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", @@ -6413,6 +6574,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/object-path": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz", @@ -6488,6 +6674,12 @@ "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" }, + "node_modules/parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6882,6 +7074,34 @@ } ] }, + "node_modules/quill": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", + "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", + "dev": true, + "dependencies": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.2", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "dev": true, + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -7038,6 +7258,21 @@ "integrity": "sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==", "dev": true }, + "node_modules/react-quill": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz", + "integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==", + "dev": true, + "dependencies": { + "@types/quill": "^1.3.10", + "lodash": "^4.17.4", + "quill": "^1.3.7" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, "node_modules/react-redux": { "version": "7.2.9", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", @@ -7233,6 +7468,23 @@ "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", "dev": true }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -7520,6 +7772,20 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", diff --git a/package.json b/package.json index 41f4b21..af82f14 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "prettier": "^3.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-quill": "^2.0.0", "react-router-dom": "^6.20.1", "react-toastify": "^9.1.3", "socket.io-client": "^4.7.2", diff --git a/src/components/files/FilePreview.jsx b/src/components/files/FilePreview.jsx index 9920a0f..8a7d27c 100644 --- a/src/components/files/FilePreview.jsx +++ b/src/components/files/FilePreview.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, memo } from "react"; import useMediaQuery from "@mui/material/useMediaQuery"; import { useTheme } from "@mui/material/styles"; import Button from "@mui/material/Button"; @@ -8,6 +8,8 @@ import DialogActions from "@mui/material/DialogActions"; import Dialog from "@mui/material/Dialog"; import Toolbar from "@mui/material/Toolbar"; +import TextEditor from "./TextEditor.jsx"; + const textFileTypes = ["properties", "txt", "yaml", "yml", "json", "env"]; const imageFileTypes = ["png", "jpeg", "jpg"]; @@ -19,28 +21,40 @@ export function useFilePreview(isOpen = false) { return [open, dialogToggle]; } -function TextPreview(props) { - const { fileText } = props; - return
{fileText}
; -} - export default function FilePreview(props) { const [fileText, setFileText] = useState(); + const [modifiedText, setModifiedText] = useState(); const theme = useTheme(); const fullScreen = useMediaQuery(theme.breakpoints.down("md")); - const { previewData, open, dialogToggle } = props; - const { fileData, name } = previewData ?? {}; + const { previewData, open, dialogToggle, server: serverId } = props; + const { fileData, name, filePath } = previewData ?? {}; const ext = name ? name.split(".").pop() : null; const isTextFile = textFileTypes.includes(ext); - async function onPreviewChange() { - if (isTextFile) setFileText(await fileData.text()); - } - useEffect(() => { onPreviewChange(); }, [fileData]); + const editorChange = (v) => setModifiedText(v); + + async function onPreviewChange() { + if (!isTextFile) return; + const text = await fileData.text(); + setFileText(text); + } + + async function onSave() { + const formData = new FormData(); + const blob = new Blob([modifiedText], { type: "plain/text" }); + formData.append("file", blob, name); + formData.append("id", serverId); + formData.append("path", filePath); + await fetch("/api/files/upload", { + method: "POST", + body: formData, + }); + dialogToggle(); + } return ( {name} - + + ); diff --git a/src/components/files/MineclusterFiles.jsx b/src/components/files/MineclusterFiles.jsx index 31c1389..3dbc6d4 100644 --- a/src/components/files/MineclusterFiles.jsx +++ b/src/components/files/MineclusterFiles.jsx @@ -105,7 +105,7 @@ export default function MineclusterFiles(props) { const formData = new FormData(); formData.append("file", file); formData.append("id", serverId); - formData.append("path", [...dirStack, name].join("/")); + formData.append("path", [...dirStack, file.name].join("/")); await fetch("/api/files/upload", { method: "POST", body: formData, @@ -125,7 +125,8 @@ export default function MineclusterFiles(props) { function previewFile(file) { const { name } = file; previewServerItem(serverId, [...dirStack, name].join("/")).then( - (fileData) => changePreview(name, fileData), + (fileData) => + changePreview(name, fileData, [...dirStack, name].join("/")), ); } diff --git a/src/components/files/TextEditor.jsx b/src/components/files/TextEditor.jsx new file mode 100644 index 0000000..6151cf8 --- /dev/null +++ b/src/components/files/TextEditor.jsx @@ -0,0 +1,21 @@ +import ReactQuill from "react-quill"; +import { useState, useEffect, useMemo, memo } from "react"; +import "react-quill/dist/quill.snow.css"; + +const buildDelta = (t) => { + if (!t) return; + const ops = t.split("\n").map((l) => ({ insert: `${l}\n` })); + return { ops }; +}; + +function TextEditor(props) { + const { text, onChange } = props; + const [delta, setDelta] = useState(); + const constructDelta = useMemo(() => buildDelta(text), [text]); + useEffect(() => setDelta(constructDelta), [text]); + + const onEditorChange = (c, d, s, editor) => onChange(editor.getText()); + + return ; +} +export default memo(TextEditor, (a, b) => a.text === b.text); diff --git a/src/components/server-options/ExtraPortsOption.jsx b/src/components/server-options/ExtraPortsOption.jsx new file mode 100644 index 0000000..57c026e --- /dev/null +++ b/src/components/server-options/ExtraPortsOption.jsx @@ -0,0 +1,41 @@ +import { useState } from "react"; +import TextField from "@mui/material/TextField"; +import Autocomplete from "@mui/material/Autocomplete"; +import Chip from "@mui/material/Chip"; + +const validatePort = (p) => p !== "25565" && p !== "25575" && p.length < 6; + +export default function ExtraPortsOption(props) { + const [extraPorts, setExtraPorts] = useState([]); + const { onChange } = props; + + function portChange(e, val, optionType, changedValue) { + if (optionType === "clear") { + setExtraPorts([]); + onChange("extraPorts", []); + return; + } + if (!validatePort(changedValue.option)) + return alert("That port cannot be added/removed as an extra port!"); + setExtraPorts(val); + onChange("extraPorts", val); + } + + return ( + } + renderTags={(value, getTagProps) => + value.map((option, index) => { + const defaultChipProps = getTagProps({ index }); + return ; + }) + } + /> + ); +} diff --git a/src/pages/CreateCoreOptions.jsx b/src/pages/CreateCoreOptions.jsx index ee8b92f..afe2f0d 100644 --- a/src/pages/CreateCoreOptions.jsx +++ b/src/pages/CreateCoreOptions.jsx @@ -21,6 +21,7 @@ import CpuOption, { import MemoryOption, { memoryOptions, } from "@mcl/components/server-options/MemoryOption.jsx"; +import ExtraPortsOption from "@mcl/components/server-options/ExtraPortsOption.jsx"; import BackupHostOption from "@mcl/components/server-options/BackupHostOption.jsx"; import BackupBucketOption from "@mcl/components/server-options/BackupBucketOption.jsx"; @@ -35,6 +36,7 @@ const defaultServer = { serverType: serverTypeOptions[0], cpu: cpuOptions[0], memory: memoryOptions[2], // 1.5GB + extraPorts: [], }; export default function CreateCoreOptions() { @@ -79,7 +81,6 @@ export default function CreateCoreOptions() { ).toLowerCase()}`); } else for (var k in s) if (k.startsWith("backup")) delete s[k]; setSpec(s); - console.log(s); setBackupEnabled(!backupEnabled); }; @@ -98,6 +99,7 @@ export default function CreateCoreOptions() { /> + export async function previewServerItem(serverId, path) { const resp = await fetchApiCore("/files/item", { id: serverId, path }); - if (!resp.status === 200) return console.log("AHHHH"); + if (resp.status !== 200) return console.log("AHHHH"); const blob = await resp.blob(); return blob; }