diff --git a/package.json b/package.json index 75e5b87b3..3dacba4bb 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,12 @@ "react-router-dom": "^5.0.1", "react-transition-group": "^4.1.0", "socket.io-client": "^2.2.0", + "sockette": "^2.0.6", "use-react-router": "^1.0.7", "ws-wrapper": "^2.0.0", - "xterm": "^3.5.1", + "xterm": "^3.14.4", + "xterm-addon-attach": "^0.1.0", + "xterm-addon-fit": "^0.1.0", "yup": "^0.27.0" }, "devDependencies": { diff --git a/resources/scripts/api/server/getServer.ts b/resources/scripts/api/server/getServer.ts new file mode 100644 index 000000000..e56a0f780 --- /dev/null +++ b/resources/scripts/api/server/getServer.ts @@ -0,0 +1,50 @@ +import http from '@/api/http'; + +export interface Allocation { + ip: string; + alias: string | null; + port: number; +} + +export interface Server { + id: string; + uuid: string; + name: string; + node: string; + description: string; + allocations: Allocation[]; + limits: { + memory: number; + swap: number; + disk: number; + io: number; + cpu: number; + }; + featureLimits: { + databases: number; + allocations: number; + }; +} + +export const rawDataToServerObject = (data: any): Server => ({ + id: data.identifier, + uuid: data.uuid, + name: data.name, + node: data.node, + description: data.description ? ((data.description.length > 0) ? data.description : null) : null, + allocations: [{ + ip: data.allocation.ip, + alias: null, + port: data.allocation.port, + }], + limits: { ...data.limits }, + featureLimits: { ...data.feature_limits }, +}); + +export default (uuid: string): Promise => { + return new Promise((resolve, reject) => { + http.get(`/api/client/servers/${uuid}`) + .then(response => resolve(rawDataToServerObject(response.data.attributes))) + .catch(reject); + }); +}; diff --git a/resources/scripts/components/dashboard/DashboardContainer.tsx b/resources/scripts/components/dashboard/DashboardContainer.tsx index 4aecf9ea8..052d00034 100644 --- a/resources/scripts/components/dashboard/DashboardContainer.tsx +++ b/resources/scripts/components/dashboard/DashboardContainer.tsx @@ -9,7 +9,7 @@ import { Link } from 'react-router-dom'; export default () => (
- +
diff --git a/resources/scripts/components/elements/Spinner.tsx b/resources/scripts/components/elements/Spinner.tsx new file mode 100644 index 000000000..090980e3f --- /dev/null +++ b/resources/scripts/components/elements/Spinner.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import classNames from 'classnames'; + +export default ({ large }: { large?: boolean }) => ( +
+); diff --git a/resources/scripts/components/elements/SpinnerOverlay.tsx b/resources/scripts/components/elements/SpinnerOverlay.tsx index fa7cb1ea4..92eb43cf1 100644 --- a/resources/scripts/components/elements/SpinnerOverlay.tsx +++ b/resources/scripts/components/elements/SpinnerOverlay.tsx @@ -1,6 +1,7 @@ import React from 'react'; import classNames from 'classnames'; import { CSSTransition } from 'react-transition-group'; +import Spinner from '@/components/elements/Spinner'; export default ({ large, visible }: { visible: boolean; large?: boolean }) => ( @@ -8,7 +9,7 @@ export default ({ large, visible }: { visible: boolean; large?: boolean }) => ( className={classNames('absolute pin-t pin-l flex items-center justify-center w-full h-full rounded')} style={{ background: 'rgba(0, 0, 0, 0.45)' }} > -
+
); diff --git a/resources/scripts/components/server/Console.tsx b/resources/scripts/components/server/Console.tsx new file mode 100644 index 000000000..f30d35a6a --- /dev/null +++ b/resources/scripts/components/server/Console.tsx @@ -0,0 +1,71 @@ +import React, { createRef, useEffect, useRef } from 'react'; +import { Terminal } from 'xterm'; +import * as TerminalFit from 'xterm/lib/addons/fit/fit'; +import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; + +const theme = { + background: 'transparent', + cursor: 'transparent', + black: '#000000', + red: '#E54B4B', + green: '#9ECE58', + yellow: '#FAED70', + blue: '#396FE2', + magenta: '#BB80B3', + cyan: '#2DDAFD', + white: '#d0d0d0', + brightBlack: 'rgba(255, 255, 255, 0.2)', + brightRed: '#FF5370', + brightGreen: '#C3E88D', + brightYellow: '#FFCB6B', + brightBlue: '#82AAFF', + brightMagenta: '#C792EA', + brightCyan: '#89DDFF', + brightWhite: '#ffffff', +}; + +export default () => { + const ref = createRef(); + + const terminal = useRef(new Terminal({ + disableStdin: true, + cursorStyle: 'underline', + allowTransparency: true, + fontSize: 12, + fontFamily: 'Menlo, Monaco, Consolas, monospace', + rows: 30, + theme: theme, + })); + + useEffect(() => { + ref.current && terminal.current.open(ref.current); + + // @see https://github.com/xtermjs/xterm.js/issues/2265 + // @see https://github.com/xtermjs/xterm.js/issues/2230 + TerminalFit.fit(terminal.current); + + terminal.current.writeln('Testing console data'); + terminal.current.writeln('Testing other data'); + }, []); + + return ( +
+ +
+
+
+
+
$
+
+ +
+
+
+ ); +}; diff --git a/resources/scripts/components/server/ServerConsole.tsx b/resources/scripts/components/server/ServerConsole.tsx index aae89742c..50ccf8519 100644 --- a/resources/scripts/components/server/ServerConsole.tsx +++ b/resources/scripts/components/server/ServerConsole.tsx @@ -1,7 +1,13 @@ import React from 'react'; +import Console from '@/components/server/Console'; export default () => ( -
- Test +
+
+ +
+
+

Testing

+
); diff --git a/resources/scripts/components/server/WebsocketHandler.tsx b/resources/scripts/components/server/WebsocketHandler.tsx new file mode 100644 index 000000000..0fd1f0114 --- /dev/null +++ b/resources/scripts/components/server/WebsocketHandler.tsx @@ -0,0 +1,34 @@ +import React, { useEffect } from 'react'; +import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy'; +import { ApplicationState } from '@/state/types'; +import Sockette from 'sockette'; + +export default () => { + const server = useStoreState((state: State) => state.server.data); + const instance = useStoreState((state: State) => state.server.socket.instance); + const setInstance = useStoreActions((actions: Actions) => actions.server.socket.setInstance); + const setConnectionState = useStoreActions((actions: Actions) => actions.server.socket.setConnectionState); + + useEffect(() => { + // If there is already an instance or there is no server, just exit out of this process + // since we don't need to make a new connection. + if (instance || !server) { + return; + } + + console.log('need to connect to instance'); + const socket = new Sockette(`wss://wings.pterodactyl.test:8080/api/servers/${server.uuid}/ws`, { + protocols: 'CC8kHCuMkXPosgzGO6d37wvhNcksWxG6kTrA', + // onmessage: (ev) => console.log(ev), + onopen: () => setConnectionState(true), + onclose: () => setConnectionState(false), + onerror: () => setConnectionState(false), + }); + + console.log('Setting instance!'); + + setInstance(socket); + }, [server]); + + return null; +}; diff --git a/resources/scripts/routers/ServerRouter.tsx b/resources/scripts/routers/ServerRouter.tsx index 0e8543690..62f5ff8e0 100644 --- a/resources/scripts/routers/ServerRouter.tsx +++ b/resources/scripts/routers/ServerRouter.tsx @@ -1,28 +1,52 @@ -import * as React from 'react'; +import React, { useEffect } from 'react'; import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom'; import NavigationBar from '@/components/NavigationBar'; import ServerConsole from '@/components/server/ServerConsole'; import TransitionRouter from '@/TransitionRouter'; +import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy'; +import { ApplicationState } from '@/state/types'; +import Spinner from '@/components/elements/Spinner'; +import WebsocketHandler from '@/components/server/WebsocketHandler'; -export default ({ match, location }: RouteComponentProps) => ( - - -
-
-
- Console - File Manager - Databases - User Management +export default ({ match, location }: RouteComponentProps<{ id: string }>) => { + const server = useStoreState((state: State) => state.server.data); + const { clearServerState, getServer } = useStoreActions((actions: Actions) => actions.server); + + if (!server) { + getServer(match.params.id); + } + + useEffect(() => () => clearServerState(), []); + + return ( + + +
+
+
+ Console + File Manager + Databases + User Management +
-
- -
- - - -
-
- -); + +
+ {!server ? +
+ +
+ : + + + + + + + } +
+
+ + ); +}; diff --git a/resources/scripts/state/index.ts b/resources/scripts/state/index.ts index 5df635796..5e19365f8 100644 --- a/resources/scripts/state/index.ts +++ b/resources/scripts/state/index.ts @@ -2,10 +2,12 @@ import { createStore } from 'easy-peasy'; import { ApplicationState } from '@/state/types'; import flashes from '@/state/models/flashes'; import user from '@/state/models/user'; +import server from '@/state/models/server'; const state: ApplicationState = { flashes, user, + server, }; export const store = createStore(state); diff --git a/resources/scripts/state/models/server.ts b/resources/scripts/state/models/server.ts new file mode 100644 index 000000000..1c6539eac --- /dev/null +++ b/resources/scripts/state/models/server.ts @@ -0,0 +1,34 @@ +import getServer, { Server } from '@/api/server/getServer'; +import { action, Action, thunk, Thunk } from 'easy-peasy'; +import socket, { SocketState } from './socket'; + +export interface ServerState { + data?: Server; + socket: SocketState; + getServer: Thunk>; + setServer: Action; + clearServerState: Action; +} + +const server: ServerState = { + socket, + getServer: thunk(async (actions, payload) => { + const server = await getServer(payload); + actions.setServer(server); + }), + setServer: action((state, payload) => { + state.data = payload; + }), + clearServerState: action(state => { + state.data = undefined; + + if (state.socket.instance) { + state.socket.instance.close(); + } + + state.socket.instance = null; + state.socket.connected = false; + }), +}; + +export default server; diff --git a/resources/scripts/state/models/socket.ts b/resources/scripts/state/models/socket.ts new file mode 100644 index 000000000..fb5082acb --- /dev/null +++ b/resources/scripts/state/models/socket.ts @@ -0,0 +1,22 @@ +import { Action, action } from 'easy-peasy'; +import Sockette from 'sockette'; + +export interface SocketState { + instance: Sockette | null; + connected: boolean; + setInstance: Action; + setConnectionState: Action; +} + +const socket: SocketState = { + instance: null, + connected: false, + setInstance: action((state, payload) => { + state.instance = payload; + }), + setConnectionState: action((state, payload) => { + state.connected = payload; + }), +}; + +export default socket; diff --git a/resources/scripts/state/types.d.ts b/resources/scripts/state/types.d.ts index 9774f6f73..d54afc29d 100644 --- a/resources/scripts/state/types.d.ts +++ b/resources/scripts/state/types.d.ts @@ -1,10 +1,12 @@ import { FlashMessageType } from '@/components/MessageBox'; import { Action } from 'easy-peasy'; import { UserState } from '@/state/models/user'; +import { ServerState } from '@/state/models/server'; export interface ApplicationState { flashes: FlashState; user: UserState; + server: ServerState; } export interface FlashState { diff --git a/webpack.config.js b/webpack.config.js index bd0774b65..0054d91d3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -49,7 +49,7 @@ module.exports = { cache: true, target: 'web', mode: process.env.NODE_ENV, - devtool: isProduction ? false : 'cheap-eval-source-map', + devtool: isProduction ? false : 'eval-source-map', performance: { hints: false, }, diff --git a/yarn.lock b/yarn.lock index 974cee7d7..102a9feff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6925,6 +6925,10 @@ socket.io-parser@~3.3.0: debug "~3.1.0" isarray "2.0.1" +sockette@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/sockette/-/sockette-2.0.6.tgz#63b533f3cfe3b592fc84178beea6577fa18cebf3" + sockjs-client@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177" @@ -8015,9 +8019,17 @@ xtend@^4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" -xterm@^3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.5.1.tgz#d2e62ab26108a771b7bd1b7be4f6578fb4aff922" +xterm-addon-attach@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/xterm-addon-attach/-/xterm-addon-attach-0.1.0.tgz#e0daa8188e9bb830def9ccad015fc62bc07e3abe" + +xterm-addon-fit@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.1.0.tgz#dd52d8b2ec6ef05faab8285bafd9310063704468" + +xterm@^3.14.4: + version "3.14.4" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.14.4.tgz#68a474fd0628e6027e420f6c8b0df136f6281ff8" y18n@^3.2.1: version "3.2.1"