Switch to a context store for server stuff to better support things in the future

This commit is contained in:
Dane Everitt 2019-07-09 21:25:57 -07:00
parent 16e6f3f45f
commit 986285402f
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
21 changed files with 218 additions and 148 deletions

View File

@ -0,0 +1,31 @@
import http from '@/api/http';
export interface ServerDatabase {
id: string;
name: string;
username: string;
connectionString: string;
allowConnectionsFrom: string;
password?: string;
}
export const rawDataToServerDatabase = (data: any): ServerDatabase => ({
id: data.id,
name: data.name,
username: data.username,
connectionString: `${data.host.address}:${data.host.port}`,
allowConnectionsFrom: data.connections_from,
password: data.relationships && data.relationships.password ? data.relationships.password.attributes.password : undefined,
});
export default (uuid: string, includePassword: boolean = true): Promise<ServerDatabase[]> => {
return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${uuid}/databases`, {
params: includePassword ? { include: 'password' } : undefined,
})
.then(response => resolve(
(response.data.data || []).map((item: any) => rawDataToServerDatabase(item.attributes))
))
.catch(reject);
});
};

View File

@ -1,7 +1,7 @@
import React from 'react';
import MessageBox from '@/components/MessageBox';
import { State, useStoreState } from 'easy-peasy';
import { ApplicationState } from '@/state/types';
import { ApplicationStore } from '@/state';
type Props = Readonly<{
byKey?: string;
@ -10,7 +10,7 @@ type Props = Readonly<{
}>;
export default ({ withBottomSpace, spacerClass, byKey }: Props) => {
const flashes = useStoreState((state: State<ApplicationState>) => state.flashes.items);
const flashes = useStoreState((state: State<ApplicationStore>) => state.flashes.items);
let filtered = flashes;
if (byKey) {

View File

@ -4,14 +4,14 @@ import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail';
import { httpErrorToHuman } from '@/api/http';
import LoginFormContainer from '@/components/auth/LoginFormContainer';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationState } from '@/state/types';
import FlashMessageRender from '@/components/FlashMessageRender';
import { ApplicationStore } from '@/state';
export default () => {
const [ isSubmitting, setSubmitting ] = React.useState(false);
const [ email, setEmail ] = React.useState('');
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationState>) => actions.flashes);
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const handleFieldUpdate = (e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value);

View File

@ -4,15 +4,15 @@ import loginCheckpoint from '@/api/auth/loginCheckpoint';
import { httpErrorToHuman } from '@/api/http';
import LoginFormContainer from '@/components/auth/LoginFormContainer';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationState } from '@/state/types';
import { StaticContext } from 'react-router';
import FlashMessageRender from '@/components/FlashMessageRender';
import { ApplicationStore } from '@/state';
export default ({ history, location: { state } }: RouteComponentProps<{}, StaticContext, { token?: string }>) => {
const [ code, setCode ] = useState('');
const [ isLoading, setIsLoading ] = useState(false);
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationState>) => actions.flashes);
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
if (!state || !state.token) {
history.replace('/auth/login');

View File

@ -5,14 +5,14 @@ import { httpErrorToHuman } from '@/api/http';
import LoginFormContainer from '@/components/auth/LoginFormContainer';
import FlashMessageRender from '@/components/FlashMessageRender';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationState } from '@/state/types';
import { ApplicationStore } from '@/state';
export default ({ history }: RouteComponentProps) => {
const [ username, setUsername ] = useState('');
const [ password, setPassword ] = useState('');
const [ isLoading, setLoading ] = useState(false);
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationState>) => actions.flashes);
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

View File

@ -7,7 +7,7 @@ import { httpErrorToHuman } from '@/api/http';
import LoginFormContainer from '@/components/auth/LoginFormContainer';
import FlashMessageRender from '@/components/FlashMessageRender';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationState } from '@/state/types';
import { ApplicationStore } from '@/state';
type Props = Readonly<RouteComponentProps<{ token: string }> & {}>;
@ -17,7 +17,7 @@ export default (props: Props) => {
const [ password, setPassword ] = useState('');
const [ passwordConfirm, setPasswordConfirm ] = useState('');
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationState>) => actions.flashes);
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const parsed = parse(props.location.search);
if (email.length === 0 && parsed.email) {

View File

@ -1,11 +1,11 @@
import React from 'react';
import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy';
import { ApplicationState } from '@/state/types';
import { Form, Formik, FormikActions } from 'formik';
import * as Yup from 'yup';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import Field from '@/components/elements/Field';
import { httpErrorToHuman } from '@/api/http';
import { ApplicationStore } from '@/state';
interface Values {
email: string;
@ -18,10 +18,10 @@ const schema = Yup.object().shape({
});
export default () => {
const user = useStoreState((state: State<ApplicationState>) => state.user.data);
const updateEmail = useStoreActions((state: Actions<ApplicationState>) => state.user.updateUserEmail);
const user = useStoreState((state: State<ApplicationStore>) => state.user.data);
const updateEmail = useStoreActions((state: Actions<ApplicationStore>) => state.user.updateUserEmail);
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationState>) => actions.flashes);
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const submit = (values: Values, { resetForm, setSubmitting }: FormikActions<Values>) => {
clearFlashes('account:email');

View File

@ -1,12 +1,12 @@
import React from 'react';
import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy';
import { ApplicationState } from '@/state/types';
import { Form, Formik, FormikActions } from 'formik';
import Field from '@/components/elements/Field';
import * as Yup from 'yup';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import updateAccountPassword from '@/api/account/updateAccountPassword';
import { httpErrorToHuman } from '@/api/http';
import { ApplicationStore } from '@/state';
interface Values {
current: string;
@ -23,8 +23,8 @@ const schema = Yup.object().shape({
});
export default () => {
const user = useStoreState((state: State<ApplicationState>) => state.user.data);
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationState>) => actions.flashes);
const user = useStoreState((state: State<ApplicationStore>) => state.user.data);
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
if (!user) {
return null;

View File

@ -2,9 +2,9 @@ import React, { createRef } from 'react';
import { Terminal } from 'xterm';
import * as TerminalFit from 'xterm/lib/addons/fit/fit';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import { ApplicationState } from '@/state/types';
import { connect } from 'react-redux';
import { Websocket } from '@/plugins/Websocket';
import { ServerStore } from '@/state/server';
const theme = {
background: 'transparent',
@ -113,8 +113,8 @@ class Console extends React.PureComponent<Readonly<Props>> {
}
export default connect(
(state: ApplicationState) => ({
connected: state.server.socket.connected,
instance: state.server.socket.instance,
(state: ServerStore) => ({
connected: state.socket.connected,
instance: state.socket.instance,
}),
)(Console);

View File

@ -1,10 +1,9 @@
import React from 'react';
import Console from '@/components/server/Console';
import { State, useStoreState } from 'easy-peasy';
import { ApplicationState } from '@/state/types';
import { ServerContext } from '@/state/server';
export default () => {
const status = useStoreState((state: State<ApplicationState>) => state.server.status);
const status = ServerContext.useStoreState(state => state.status.value);
return (
<div className={'my-10 flex'}>

View File

@ -0,0 +1,24 @@
import React, { useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faDatabase } from '@fortawesome/free-solid-svg-icons/faDatabase';
import getServerDatabases from '@/api/server/getServerDatabases';
import { useStoreState } from 'easy-peasy';
export default () => {
useEffect(() => {
getServerDatabases('s');
}, []);
return (
<div className={'my-10'}>
<div className={'flex rounded no-underline text-neutral-200 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={faDatabase}/>
</div>
<div className={'w-1/2 ml-4'}>
<p className={'text-lg'}>sfgsfgd</p>
</div>
</div>
</div>
);
};

View File

@ -1,13 +1,12 @@
import React, { useEffect } from 'react';
import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy';
import { ApplicationState } from '@/state/types';
import { Websocket } from '@/plugins/Websocket';
import { ServerContext } from '@/state/server';
export default () => {
const server = useStoreState((state: State<ApplicationState>) => state.server.data);
const instance = useStoreState((state: State<ApplicationState>) => state.server.socket.instance);
const setServerStatus = useStoreActions((actions: Actions<ApplicationState>) => actions.server.setServerStatus);
const { setInstance, setConnectionState } = useStoreActions((actions: Actions<ApplicationState>) => actions.server.socket);
const server = ServerContext.useStoreState(state => state.server.data);
const instance = ServerContext.useStoreState(state => state.socket.instance);
const setServerStatus = ServerContext.useStoreActions(actions => actions.status.setServerStatus);
const { setInstance, setConnectionState } = ServerContext.useStoreActions(actions => actions.socket);
useEffect(() => {
// If there is already an instance or there is no server, just exit out of this process
@ -20,7 +19,7 @@ export default () => {
const socket = new Websocket(
`wss://wings.pterodactyl.test:8080/api/servers/${server.uuid}/ws`,
'CC8kHCuMkXPosgzGO6d37wvhNcksWxG6kTrA'
'CC8kHCuMkXPosgzGO6d37wvhNcksWxG6kTrA',
);
socket.on('SOCKET_OPEN', () => setConnectionState(true));

View File

@ -3,14 +3,16 @@ 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';
import ServerDatabases from '@/components/server/ServerDatabases';
import { ServerContext } from '@/state/server';
import { Provider } from 'react-redux';
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);
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
const server = ServerContext.useStoreState(state => state.server.data);
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState);
if (!server) {
getServer(match.params.id);
@ -31,22 +33,31 @@ export default ({ match, location }: RouteComponentProps<{ id: string }>) => {
</div>
</div>
</div>
<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>
<Provider store={ServerContext.useStore()}>
<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/>
<Route path={`${match.path}/databases`} component={ServerDatabases}/>
</Switch>
</React.Fragment>
}
</div>
</TransitionRouter>
</Provider>
</React.Fragment>
);
};
export default (props: RouteComponentProps<any>) => (
<ServerContext.Provider>
<ServerRouter {...props}/>
</ServerContext.Provider>
);

View File

@ -0,0 +1,28 @@
import { Action, action } from 'easy-peasy';
import { FlashMessageType } from '@/components/MessageBox';
export interface FlashStore {
items: FlashMessage[];
addFlash: Action<FlashStore, FlashMessage>;
clearFlashes: Action<FlashStore, string | void>;
}
export interface FlashMessage {
id?: string;
key?: string;
type: FlashMessageType;
title?: string;
message: string;
}
const flashes: FlashStore = {
items: [],
addFlash: action((state, payload) => {
state.items.push(payload);
}),
clearFlashes: action((state, payload) => {
state.items = payload ? state.items.filter(flashes => flashes.key !== payload) : [];
}),
};
export default flashes;

View File

@ -1,13 +1,15 @@
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';
import flashes, { FlashStore } from '@/state/flashes';
import user, { UserStore } from '@/state/user';
const state: ApplicationState = {
export interface ApplicationStore {
flashes: FlashStore;
user: UserStore;
}
const state: ApplicationStore = {
flashes,
user,
server,
};
export const store = createStore(state);

View File

@ -1,14 +0,0 @@
import { action } from 'easy-peasy';
import { FlashState } from '@/state/types';
const flashes: FlashState = {
items: [],
addFlash: action((state, payload) => {
state.items.push(payload);
}),
clearFlashes: action((state, payload) => {
state.items = payload ? state.items.filter(flashes => flashes.key !== payload) : [];
}),
};
export default flashes;

View File

@ -1,43 +0,0 @@
import getServer, { Server } from '@/api/server/getServer';
import { action, Action, thunk, Thunk } from 'easy-peasy';
import socket, { SocketState } from './socket';
export type ServerStatus = 'offline' | 'starting' | 'stopping' | 'running';
export interface ServerState {
data?: Server;
status: ServerStatus;
socket: SocketState;
getServer: Thunk<ServerState, string, {}, any, Promise<void>>;
setServer: Action<ServerState, Server>;
setServerStatus: Action<ServerState, ServerStatus>;
clearServerState: Action<ServerState>;
}
const server: ServerState = {
socket,
status: 'offline',
getServer: thunk(async (actions, payload) => {
const server = await getServer(payload);
actions.setServer(server);
}),
setServer: action((state, payload) => {
state.data = payload;
}),
setServerStatus: action((state, payload) => {
state.status = payload;
}),
clearServerState: action(state => {
state.data = undefined;
if (state.socket.instance) {
state.socket.instance.removeAllListeners();
state.socket.instance.close();
}
state.socket.instance = null;
state.socket.connected = false;
}),
};
export default server;

View File

@ -0,0 +1,57 @@
import getServer, { Server } from '@/api/server/getServer';
import { action, Action, createContextStore, thunk, Thunk } from 'easy-peasy';
import socket, { SocketStore } from './socket';
export type ServerStatus = 'offline' | 'starting' | 'stopping' | 'running';
interface ServerDataStore {
data?: Server;
getServer: Thunk<ServerDataStore, string, {}, any, Promise<void>>;
setServer: Action<ServerDataStore, Server>;
}
const server: ServerDataStore = {
getServer: thunk(async (actions, payload) => {
const server = await getServer(payload);
actions.setServer(server);
}),
setServer: action((state, payload) => {
state.data = payload;
}),
};
interface ServerStatusStore {
value: ServerStatus;
setServerStatus: Action<ServerStatusStore, ServerStatus>;
}
const status: ServerStatusStore = {
value: 'offline',
setServerStatus: action((state, payload) => {
state.value = payload;
}),
};
export interface ServerStore {
server: ServerDataStore;
socket: SocketStore;
status: ServerStatusStore;
clearServerState: Action<ServerStore>;
}
export const ServerContext = createContextStore<ServerStore>({
server,
socket,
status,
clearServerState: action(state => {
state.server.data = undefined;
if (state.socket.instance) {
state.socket.instance.removeAllListeners();
state.socket.instance.close();
}
state.socket.instance = null;
state.socket.connected = false;
}),
}, { name: 'ServerStore' });

View File

@ -1,14 +1,14 @@
import { Action, action } from 'easy-peasy';
import { Websocket } from '@/plugins/Websocket';
export interface SocketState {
export interface SocketStore {
instance: Websocket | null;
connected: boolean;
setInstance: Action<SocketState, Websocket | null>;
setConnectionState: Action<SocketState, boolean>;
setInstance: Action<SocketStore, Websocket | null>;
setConnectionState: Action<SocketStore, boolean>;
}
const socket: SocketState = {
const socket: SocketStore = {
instance: null,
connected: false,
setInstance: action((state, payload) => {

View File

@ -1,24 +0,0 @@
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 {
items: FlashMessage[];
addFlash: Action<FlashState, FlashMessage>;
clearFlashes: Action<FlashState, string | void>;
}
export interface FlashMessage {
id?: string;
key?: string;
type: FlashMessageType;
title?: string;
message: string;
}

View File

@ -12,14 +12,14 @@ export interface UserData {
updatedAt: Date;
}
export interface UserState {
export interface UserStore {
data?: UserData;
setUserData: Action<UserState, UserData>;
updateUserData: Action<UserState, Partial<UserData>>;
updateUserEmail: Thunk<UserState, { email: string; password: string }, any, {}, Promise<void>>;
setUserData: Action<UserStore, UserData>;
updateUserData: Action<UserStore, Partial<UserData>>;
updateUserEmail: Thunk<UserStore, { email: string; password: string }, any, {}, Promise<void>>;
}
const user: UserState = {
const user: UserStore = {
data: undefined,
setUserData: action((state, payload) => {
state.data = payload;