forked from Alex/Pterodactyl-Panel
Handle connecting to websocket instance
Very beta code for handling sockets
This commit is contained in:
parent
6618a124e7
commit
f0ca8bc3a3
@ -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": {
|
||||
|
50
resources/scripts/api/server/getServer.ts
Normal file
50
resources/scripts/api/server/getServer.ts
Normal file
@ -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<Server> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/servers/${uuid}`)
|
||||
.then(response => resolve(rawDataToServerObject(response.data.attributes)))
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
@ -9,7 +9,7 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
export default () => (
|
||||
<div className={'my-10'}>
|
||||
<Link to={'/server/123'} className={'flex no-underline text-neutral-200 cursor-pointer items-center bg-neutral-700 p-4 border border-transparent hover:border-neutral-500'}>
|
||||
<Link to={'/server/e9d6c836'} className={'flex no-underline text-neutral-200 cursor-pointer items-center bg-neutral-700 p-4 border border-transparent hover:border-neutral-500'}>
|
||||
<div className={'rounded-full bg-neutral-500 p-3'}>
|
||||
<FontAwesomeIcon icon={faServer}/>
|
||||
</div>
|
||||
|
6
resources/scripts/components/elements/Spinner.tsx
Normal file
6
resources/scripts/components/elements/Spinner.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default ({ large }: { large?: boolean }) => (
|
||||
<div className={classNames('spinner-circle spinner-white', { 'spinner-lg': large })}/>
|
||||
);
|
@ -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 }) => (
|
||||
<CSSTransition timeout={150} classNames={'fade'} in={visible} unmountOnExit={true}>
|
||||
@ -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)' }}
|
||||
>
|
||||
<div className={classNames('spinner-circle spinner-white', { 'spinner-lg': large })}></div>
|
||||
<Spinner large={large}/>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
);
|
||||
|
71
resources/scripts/components/server/Console.tsx
Normal file
71
resources/scripts/components/server/Console.tsx
Normal file
@ -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<HTMLDivElement>();
|
||||
|
||||
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 (
|
||||
<div className={'text-xs font-mono relative'}>
|
||||
<SpinnerOverlay visible={true} large={true}/>
|
||||
<div
|
||||
className={'rounded-t p-2 bg-black overflow-scroll w-full'}
|
||||
style={{
|
||||
minHeight: '16rem',
|
||||
maxHeight: '64rem',
|
||||
}}
|
||||
>
|
||||
<div id={'terminal'} ref={ref}/>
|
||||
</div>
|
||||
<div className={'rounded-b bg-neutral-900 text-neutral-100 flex'}>
|
||||
<div className={'flex-no-shrink p-2 font-bold'}>$</div>
|
||||
<div className={'w-full'}>
|
||||
<input type={'text'} className={'bg-transparent text-neutral-100 p-2 pl-0 w-full'}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,7 +1,13 @@
|
||||
import React from 'react';
|
||||
import Console from '@/components/server/Console';
|
||||
|
||||
export default () => (
|
||||
<div className={'my-10'}>
|
||||
Test
|
||||
<div className={'my-10 flex'}>
|
||||
<div className={'mx-4 w-3/4 mr-4'}>
|
||||
<Console/>
|
||||
</div>
|
||||
<div className={'flex-1 ml-4'}>
|
||||
<p>Testing</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
34
resources/scripts/components/server/WebsocketHandler.tsx
Normal file
34
resources/scripts/components/server/WebsocketHandler.tsx
Normal file
@ -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<ApplicationState>) => state.server.data);
|
||||
const instance = useStoreState((state: State<ApplicationState>) => state.server.socket.instance);
|
||||
const setInstance = useStoreActions((actions: Actions<ApplicationState>) => actions.server.socket.setInstance);
|
||||
const setConnectionState = useStoreActions((actions: Actions<ApplicationState>) => 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;
|
||||
};
|
@ -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) => (
|
||||
<React.Fragment>
|
||||
<NavigationBar/>
|
||||
<div id={'sub-navigation'}>
|
||||
<div className={'mx-auto'} style={{ maxWidth: '1200px' }}>
|
||||
<div className={'items'}>
|
||||
<NavLink to={`${match.url}`} exact>Console</NavLink>
|
||||
<NavLink to={`${match.url}/files`}>File Manager</NavLink>
|
||||
<NavLink to={`${match.url}/databases`}>Databases</NavLink>
|
||||
<NavLink to={`${match.url}/users`}>User Management</NavLink>
|
||||
export default ({ match, location }: RouteComponentProps<{ id: string }>) => {
|
||||
const server = useStoreState((state: State<ApplicationState>) => state.server.data);
|
||||
const { clearServerState, getServer } = useStoreActions((actions: Actions<ApplicationState>) => actions.server);
|
||||
|
||||
if (!server) {
|
||||
getServer(match.params.id);
|
||||
}
|
||||
|
||||
useEffect(() => () => clearServerState(), []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NavigationBar/>
|
||||
<div id={'sub-navigation'}>
|
||||
<div className={'mx-auto'} style={{ maxWidth: '1200px' }}>
|
||||
<div className={'items'}>
|
||||
<NavLink to={`${match.url}`} exact>Console</NavLink>
|
||||
<NavLink to={`${match.url}/files`}>File Manager</NavLink>
|
||||
<NavLink to={`${match.url}/databases`}>Databases</NavLink>
|
||||
<NavLink to={`${match.url}/users`}>User Management</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TransitionRouter>
|
||||
<div className={'w-full mx-auto'} style={{ maxWidth: '1200px' }}>
|
||||
<Switch location={location}>
|
||||
<Route path={`${match.path}`} component={ServerConsole} exact/>
|
||||
</Switch>
|
||||
</div>
|
||||
</TransitionRouter>
|
||||
</React.Fragment>
|
||||
);
|
||||
<TransitionRouter>
|
||||
<div className={'w-full mx-auto'} style={{ maxWidth: '1200px' }}>
|
||||
{!server ?
|
||||
<div className={'flex justify-center m-20'}>
|
||||
<Spinner large={true}/>
|
||||
</div>
|
||||
:
|
||||
<React.Fragment>
|
||||
<WebsocketHandler/>
|
||||
<Switch location={location}>
|
||||
<Route path={`${match.path}`} component={ServerConsole} exact/>
|
||||
</Switch>
|
||||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
</TransitionRouter>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
@ -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);
|
||||
|
34
resources/scripts/state/models/server.ts
Normal file
34
resources/scripts/state/models/server.ts
Normal file
@ -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<ServerState, string, {}, any, Promise<void>>;
|
||||
setServer: Action<ServerState, Server>;
|
||||
clearServerState: Action<ServerState>;
|
||||
}
|
||||
|
||||
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;
|
22
resources/scripts/state/models/socket.ts
Normal file
22
resources/scripts/state/models/socket.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Action, action } from 'easy-peasy';
|
||||
import Sockette from 'sockette';
|
||||
|
||||
export interface SocketState {
|
||||
instance: Sockette | null;
|
||||
connected: boolean;
|
||||
setInstance: Action<SocketState, Sockette | null>;
|
||||
setConnectionState: Action<SocketState, boolean>;
|
||||
}
|
||||
|
||||
const socket: SocketState = {
|
||||
instance: null,
|
||||
connected: false,
|
||||
setInstance: action((state, payload) => {
|
||||
state.instance = payload;
|
||||
}),
|
||||
setConnectionState: action((state, payload) => {
|
||||
state.connected = payload;
|
||||
}),
|
||||
};
|
||||
|
||||
export default socket;
|
2
resources/scripts/state/types.d.ts
vendored
2
resources/scripts/state/types.d.ts
vendored
@ -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 {
|
||||
|
@ -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,
|
||||
},
|
||||
|
18
yarn.lock
18
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"
|
||||
|
Loading…
Reference in New Issue
Block a user