Handle connecting to websocket instance

Very beta code for handling sockets
This commit is contained in:
Dane Everitt 2019-06-29 16:14:32 -07:00
parent 6618a124e7
commit f0ca8bc3a3
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
15 changed files with 297 additions and 30 deletions

View File

@ -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": {

View 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);
});
};

View File

@ -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>

View 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 })}/>
);

View File

@ -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>
);

View 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>
);
};

View File

@ -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>
);

View 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;
};

View File

@ -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>
);
};

View File

@ -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);

View 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;

View 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;

View File

@ -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 {

View File

@ -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,
},

View File

@ -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"