summaryrefslogtreecommitdiff
path: root/server/web/src
diff options
context:
space:
mode:
authorAL-LCL <alvin@alvinhavel.com>2023-05-19 10:39:49 +0200
committerAL-LCL <alvin@alvinhavel.com>2023-05-19 10:39:49 +0200
commit58ebd3bc0f00c532e97e9a5571471ffab87934ba (patch)
tree6e099e59af07206df6edf2b0c585d0c5a466d4bd /server/web/src
GOD-VIEWHEADmain
Diffstat (limited to 'server/web/src')
-rw-r--r--server/web/src/App.tsx82
-rw-r--r--server/web/src/components/Alert.tsx14
-rw-r--r--server/web/src/components/Card.tsx80
-rw-r--r--server/web/src/components/Footer.tsx347
-rw-r--r--server/web/src/components/Select.tsx56
-rw-r--r--server/web/src/components/Server.tsx571
-rw-r--r--server/web/src/components/Sidebar.tsx136
-rw-r--r--server/web/src/components/Window.tsx308
-rw-r--r--server/web/src/design/GlobalStyle.tsx52
-rw-r--r--server/web/src/design/components/Alert.design.tsx48
-rw-r--r--server/web/src/design/components/Card.design.tsx60
-rw-r--r--server/web/src/design/components/Footer.design.tsx132
-rw-r--r--server/web/src/design/components/Select.design.tsx37
-rw-r--r--server/web/src/design/components/Server.design.tsx120
-rw-r--r--server/web/src/design/components/Sidebar.design.tsx108
-rw-r--r--server/web/src/design/components/Window.design.tsx209
-rw-r--r--server/web/src/index.tsx56
-rw-r--r--server/web/src/interfaces/Activity.interface.ts6
-rw-r--r--server/web/src/interfaces/Alert.interface.ts4
-rw-r--r--server/web/src/interfaces/AllReducer.interface.ts8
-rw-r--r--server/web/src/interfaces/Client.interface.ts11
-rw-r--r--server/web/src/interfaces/Stream.interface.ts4
-rw-r--r--server/web/src/interfaces/components/Card.interface.ts7
-rw-r--r--server/web/src/interfaces/components/Footer.interface.ts8
-rw-r--r--server/web/src/interfaces/components/Select.interface.ts12
-rw-r--r--server/web/src/interfaces/components/Server.interface.ts15
-rw-r--r--server/web/src/interfaces/components/Sidebar.interface.ts8
-rw-r--r--server/web/src/interfaces/components/Window.interface.ts22
-rw-r--r--server/web/src/interfaces/design/AlertDesign.interface.ts3
-rw-r--r--server/web/src/interfaces/design/FooterDesign.interface.ts3
-rw-r--r--server/web/src/interfaces/design/ServerDesign.interface.ts7
-rw-r--r--server/web/src/interfaces/design/SidebarDesign.interface.ts3
-rw-r--r--server/web/src/react-app-env.d.ts1
-rw-r--r--server/web/src/redux/actions.ts55
-rw-r--r--server/web/src/redux/allReducer.ts83
-rw-r--r--server/web/src/redux/store.ts4
36 files changed, 2680 insertions, 0 deletions
diff --git a/server/web/src/App.tsx b/server/web/src/App.tsx
new file mode 100644
index 0000000..0ad4620
--- /dev/null
+++ b/server/web/src/App.tsx
@@ -0,0 +1,82 @@
+import { IAllReducer } from './interfaces/AllReducer.interface';
+import { IActivity } from './interfaces/Activity.interface';
+import { IClient } from './interfaces/Client.interface';
+import { useDispatch, useSelector } from 'react-redux';
+import { IAlert } from './interfaces/Alert.interface';
+import Server from './components/Server';
+import { useAlert } from 'react-alert';
+import React from 'react';
+import {
+ sessionAll,
+ sessionClose,
+ sessionAdd,
+ sessionRemove,
+ clientAdd,
+ clientRemove,
+ activityUpdate
+} from './redux/actions';
+
+let sessionAllEel,
+ sessionCloseEel,
+ sessionAddEel,
+ sessionRemoveEel,
+ clientAddEel,
+ clientRemoveEel,
+ activityUpdateEel;
+
+function App() {
+ const dispatch = useDispatch(),
+ alert = useAlert();
+
+ document.title = useSelector<IAllReducer, string>((state) => {
+ return `${state.clients.size} Connected Client${state.clients.size === 1 ? '' : 's'
+ }${state.session.size > 0
+ ? ` [${state.session.size} Client Session]`
+ : ''
+ }`;
+ });
+
+ clientAddEel = (unique_id: string, client: IClient, audio_alert: boolean) => {
+ dispatch(clientAdd(unique_id, client));
+
+ if (audio_alert) {
+ const promise = new Audio('./static/alert.wav').play();
+
+ promise !== undefined &&
+ promise.catch(() => window.showAlert({
+ message: 'Failed To Play Alert Audio',
+ type: 'DANGER'
+ }));
+
+ window.showAlert({
+ message: 'Client Connected',
+ type: 'SUCCESS'
+ });
+ }
+ };
+
+ window.showAlert = (data: IAlert) =>
+ // @ts-ignore
+ alert.show(data.message, { type: data.type });
+
+ sessionRemoveEel = (unique_id: string) =>
+ dispatch(sessionRemove(unique_id));
+
+ sessionAllEel = () => dispatch(sessionAll());
+ sessionCloseEel = () => dispatch(sessionClose());
+ sessionAddEel = (unique_id: string) => dispatch(sessionAdd(unique_id));
+ clientRemoveEel = (unique_id: string) => dispatch(clientRemove(unique_id));
+ activityUpdateEel = (activity: IActivity) => dispatch(activityUpdate(activity));
+
+ window.eel.expose(sessionAllEel, 'sessionAllEel');
+ window.eel.expose(sessionCloseEel, 'sessionCloseEel');
+ window.eel.expose(sessionAddEel, 'sessionAddEel');
+ window.eel.expose(sessionRemoveEel, 'sessionRemoveEel');
+ window.eel.expose(clientAddEel, 'clientAddEel');
+ window.eel.expose(clientRemoveEel, 'clientRemoveEel');
+ window.eel.expose(activityUpdateEel, 'activityUpdateEel');
+
+ return <Server />;
+}
+
+export default App;
diff --git a/server/web/src/components/Alert.tsx b/server/web/src/components/Alert.tsx
new file mode 100644
index 0000000..3b69cdf
--- /dev/null
+++ b/server/web/src/components/Alert.tsx
@@ -0,0 +1,14 @@
+// @ts-nocheck
+import { AlertButton, AlertButtonCross } from '../design/components/Alert.design';
+import React from 'react';
+
+export const AlertTemplate = ({ style, options, message, close }) => (
+ <AlertButton style={style} color={options.type} bgColor={options.type}>
+ {message}
+ {options.type === 'SUCCESS' && '!'}
+ {options.type === 'DANGER' && '.'}
+ {options.type === 'WARNING' && '.'}
+ {options.type === 'INFO' && '.'}
+ <AlertButtonCross onClick={close}>x</AlertButtonCross>
+ </AlertButton>
+);
diff --git a/server/web/src/components/Card.tsx b/server/web/src/components/Card.tsx
new file mode 100644
index 0000000..f0e7c0e
--- /dev/null
+++ b/server/web/src/components/Card.tsx
@@ -0,0 +1,80 @@
+import { IProps, IState } from '../interfaces/components/Card.interface';
+import { FaRedo, FaTrash } from 'react-icons/fa';
+import React, { Component } from 'react';
+import svg from 'plyr/dist/plyr.svg';
+import 'plyr/dist/plyr.css';
+import flvjs from 'flv.js';
+import Plyr from 'plyr';
+import {
+ CardContainer,
+ CardHeader,
+ CardFooter,
+ CardFooterItem
+} from '../design/components/Card.design';
+
+class Card extends Component<IProps, IState> {
+ videoRef: any = React.createRef();
+ plyrPlayer: any;
+ flvPlayer: any;
+
+ componentDidMount() {
+ const video = this.videoRef.current;
+ this.createFlvPlayer();
+
+ this.plyrPlayer = new Plyr(video, {
+ iconUrl: svg,
+ controls: [
+ 'play-large',
+ 'play',
+ 'progress',
+ 'mute',
+ 'volume',
+ 'fullscreen'
+ ]
+ });
+ }
+
+ createFlvPlayer = () => {
+ const video = this.videoRef.current;
+ const { source } = this.props;
+
+ this.flvPlayer = flvjs.createPlayer({
+ type: 'flv',
+ isLive: true,
+ url: source
+ });
+ this.flvPlayer.attachMediaElement(video);
+ this.flvPlayer.load()
+ }
+
+ reload = () => {
+ this.flvPlayer.destroy();
+ this.createFlvPlayer();
+ }
+
+ remove = () => {
+ const { source, title } = this.props;
+ this.props.removeStream({ source, title });
+ }
+
+ render() {
+ const { source, title } = this.props;
+
+ return (
+ <CardContainer>
+ <CardHeader title={`${title}: ${source}`}>{title}: {source}</CardHeader>
+ <video ref={this.videoRef} data-poster="./static/poster.png" />
+ <CardFooter>
+ <CardFooterItem onClick={this.reload}>
+ <FaRedo size="0.8rem" />
+ </CardFooterItem>
+ <CardFooterItem onClick={this.remove}>
+ <FaTrash size="0.8rem" />
+ </CardFooterItem>
+ </CardFooter>
+ </CardContainer>
+ )
+ }
+}
+
+export default Card;
diff --git a/server/web/src/components/Footer.tsx b/server/web/src/components/Footer.tsx
new file mode 100644
index 0000000..4814fda
--- /dev/null
+++ b/server/web/src/components/Footer.tsx
@@ -0,0 +1,347 @@
+import { IProps, IState } from '../interfaces/components/Footer.interface';
+import React, { Component, Fragment } from 'react';
+import Window from './Window';
+import {
+ FooterDropdown,
+ FooterDropdownToggle,
+ FooterDropdownContent,
+ FooterNameSpaceButton,
+ FooterDropdownButton,
+ FooterWindowManager,
+ FooterWindowButton,
+ FooterWindowClear,
+ FooterBlock,
+ FooterMenu,
+ FooterParagraph
+} from '../design/components/Footer.design';
+import {
+ FaChevronDown,
+ FaChevronUp,
+ FaLink,
+ FaListUl,
+ FaPlus,
+ FaMinus,
+ FaSyncAlt,
+ FaTrash
+} from 'react-icons/fa';
+
+class Footer extends Component<IProps, IState> {
+ winManager: any = React.createRef();
+ column = 0;
+ row = 0;
+
+ state = {
+ showHelp: false,
+ address: '',
+ windows: [],
+ help: {}
+ };
+
+ componentDidMount() {
+ window.eel.host_eel()((address: string) =>
+ window.eel.help_eel()((help: object) =>
+ this.setState({ address: address, help: help })
+ )
+ );
+ }
+
+ componentDidUpdate() {
+ this.column = 0;
+ this.row = 0;
+ }
+
+ createWindow = (
+ requestType: string,
+ argsArray: any,
+ windowData: string
+ ) => {
+ const newWindow = [
+ requestType,
+ argsArray,
+ React.createRef(),
+ true,
+ windowData ? [windowData] : null
+ ];
+
+ const windows: any = [...this.state.windows];
+ let undefinedExists = false;
+
+ for (let i = 0; i < windows.length; i++)
+ if (windows[i] === undefined) {
+ windows[i] = newWindow;
+ undefinedExists = true;
+ break;
+ }
+
+ if (!undefinedExists)
+ windows.push(newWindow);
+
+ this.setState({ windows: windows });
+ };
+
+ removeWindow = (index: number) => (event: any) => {
+ const windows = [...this.state.windows];
+ delete windows[index];
+
+ windows.filter(Boolean).length === 0
+ ? this.clearWindows()
+ : this.setState({ windows });
+
+ event.stopPropagation();
+ };
+
+ clearWindows = () => {
+ this.winManager.current.style.zIndex = 1;
+ this.setState({ windows: [] });
+ };
+
+ windowPosition = () => {
+ if (this.column === 5) {
+ this.column = 0;
+ this.row++;
+ }
+
+ this.column++;
+
+ return {
+ // CONSTANT : px for every row and column (left & top)
+ x: this.column * 25 + this.row * 25,
+ y: this.column * 25 + 40 + this.row
+ };
+ };
+
+ windowHighlight = (ref: any) => {
+ const newZIndex = Number(this.winManager.current.style.zIndex) + 1
+ this.winManager.current.style.zIndex = newZIndex;
+ ref.current.window.current.style.zIndex = newZIndex;
+ };
+
+ windowToggle = (show: boolean, window: any) => {
+ const windows: any[] = [...this.state.windows];
+ const index: number = windows.indexOf(window);
+ windows[index][3] = show;
+
+ this.windowHighlight(window[2]);
+ this.setState({ windows });
+
+ if (!show) window[2].current.window.current.style.display = 'none';
+ else window[2].current.window.current.style.display = 'block';
+ };
+
+ windowCenter = (window: any) => (event: any) => {
+ this.windowToggle(true, window);
+ // CONSTANT : based on default styles from the CSS of the window
+ // (height & width) & the values of each column & row (left & top)
+ window[2].current.window.current.style.height = '55vh';
+ window[2].current.window.current.style.width = '40vw';
+ window[2].current.window.current.style.left = '25px';
+ window[2].current.window.current.style.top = '65px';
+
+ event.stopPropagation();
+ };
+
+ createWindowEvent = (requestType: string, argsArray: any) => () => {
+ if (argsArray.length > 0) this.createWindow(requestType, argsArray, '');
+ else
+ window.eel.execute_eel({
+ message: requestType.toLowerCase()
+ })((response: any) => {
+ response !== null
+ ? response.alert
+ ? window.showAlert({
+ message: response.message,
+ type: response.type
+ })
+ : this.createWindow(requestType, argsArray, response)
+ : window.showAlert({
+ message: `${requestType} Request Executed`,
+ type: 'INFO'
+ });
+ });
+ };
+
+ windowHighlightEvent = (ref: any) => () => this.windowHighlight(ref);
+
+ windowToggleEvent = (show: boolean, window: any) => (event: any) => {
+ this.windowToggle(show, window);
+ event.stopPropagation();
+ };
+
+ launchWebVersion = (event: any) => {
+ if (event.ctrlKey) {
+ window.open(window.location.href, '_blank');
+ window.showAlert({
+ message: 'Web Version Launched',
+ type: 'INFO'
+ });
+ } else if (event.altKey) {
+ const request = new XMLHttpRequest();
+ request.open('GET', '/logout');
+ request.send();
+ window.location.reload();
+ }
+ };
+
+ showHelpToggle = () => this.setState({ showHelp: !this.state.showHelp });
+
+ render() {
+ const { showHelp, address, windows, help } = this.state;
+
+ return (
+ <Fragment>
+ <FooterDropdown>
+ <FooterDropdownToggle onClick={this.showHelpToggle}>
+ {showHelp ? (
+ <FaChevronDown size="0.8rem" />
+ ) : (
+ <FaChevronUp size="0.8rem" />
+ )}
+ </FooterDropdownToggle>
+
+ <FooterDropdownContent active={showHelp}>
+ {Object.entries(help).map(
+ ([namespace, requests]: any, index: number) => (
+ <Fragment key={index}>
+ <FooterNameSpaceButton
+ title={`${namespace} Namespace`}
+ >
+ {namespace}
+ </FooterNameSpaceButton>{' '}
+ {requests.map(
+ (
+ [
+ available,
+ requestType,
+ argsString,
+ argsArray
+ ]: any[],
+ i: number
+ ) => (
+ <FooterDropdownButton
+ key={i}
+ title={`Available: ${available}${argsString
+ ? ` ${argsString}`
+ : ''
+ }`}
+ onClick={this.createWindowEvent(
+ requestType,
+ argsArray
+ )}
+ >
+ {available === 'Session' ? (
+ <FaLink size="0.6rem" />
+ ) : null}{' '}
+ {argsString ? (
+ <FaListUl size="0.65rem" />
+ ) : null}{' '}
+ {requestType}
+ </FooterDropdownButton>
+ )
+ )}
+ </Fragment>
+ )
+ )}
+ </FooterDropdownContent>
+ </FooterDropdown>
+
+ <FooterWindowManager>
+ <div ref={this.winManager} style={{ zIndex: 1 }}>
+ {windows.length > 0 ? (
+ <Fragment>
+ <FooterWindowClear
+ title='Remove All Windows'
+ onClick={this.clearWindows}
+ >
+ Clear Windows
+ </FooterWindowClear>
+ {windows.map((window: any, index: number) => (
+ <Fragment key={index}>
+ {window !== undefined ? (
+ <Fragment>
+ <Window
+ ref={window[2]}
+ requestType={window[0]}
+ requestArgs={window[1]}
+ pos={this.windowPosition()}
+ data={window[4]}
+ hightlight={this.windowHighlightEvent(
+ window[2]
+ )}
+ toggle={this.windowToggleEvent(
+ false,
+ window
+ )}
+ destroy={this.removeWindow(
+ index
+ )}
+ />
+ <FooterWindowButton
+ onClick={this.windowToggleEvent(
+ window[3]
+ ? false
+ : true,
+ window
+ )}
+ >
+ {window[0]}{' '}
+ {window[3] ? (
+ <FaMinus
+ size="0.7rem"
+ title={`Hide ${window[0]} Window`}
+ onClick={this.windowToggleEvent(
+ false,
+ window
+ )}
+ />
+ ) : (
+ <FaPlus
+ size="0.7rem"
+ title={`Show ${window[0]} Window`}
+ onClick={this.windowToggleEvent(
+ true,
+ window
+ )}
+ />
+ )}{' '}
+ <FaSyncAlt
+ size="0.7rem"
+ title={`Reset ${window[0]} Window Position`}
+ onClick={this.windowCenter(
+ window
+ )}
+ />{' '}
+ <FaTrash
+ size="0.7rem"
+ title={`Remove ${window[0]} Window`}
+ onClick={this.removeWindow(
+ index
+ )}
+ />
+ </FooterWindowButton>
+ </Fragment>
+ ) : null}
+ </Fragment>
+ ))}
+ </Fragment>
+ ) : (
+ <FooterBlock>
+ No Active Windows To Manage
+ </FooterBlock>
+ )}
+ </div>
+ </FooterWindowManager>
+
+ <FooterMenu>
+ <FooterParagraph
+ title={`Listening Address: ${address}`}
+ onClick={this.launchWebVersion}
+ >
+ {`Listening Address: ${address}`}
+ </FooterParagraph>
+ </FooterMenu>
+ </Fragment>
+ );
+ }
+}
+
+export default Footer;
diff --git a/server/web/src/components/Select.tsx b/server/web/src/components/Select.tsx
new file mode 100644
index 0000000..76809f6
--- /dev/null
+++ b/server/web/src/components/Select.tsx
@@ -0,0 +1,56 @@
+import { SelectContainer, SelectRow } from '../design/components/Select.design';
+import { ISelect, IState } from '../interfaces/components/Select.interface';
+import React, { Component } from 'react';
+import {
+ FaUserPlus,
+ FaUserMinus,
+ FaBan,
+ FaUnlockAlt,
+ FaTrash
+} from 'react-icons/fa';
+
+class Select extends Component<ISelect, IState> {
+ render() {
+ const {
+ show,
+ top,
+ left,
+ sessionAdd,
+ sessionRemove,
+ blacklistAdd,
+ blacklistRemove,
+ clientRemove
+ } = this.props;
+
+ return show ? (
+ <SelectContainer
+ style={{
+ top: top,
+ left: left
+ }}
+ >
+ <SelectRow onClick={sessionAdd}>
+ <FaUserPlus size="1rem" color="rgb(0, 255, 255)" /> Session
+ Add
+ </SelectRow>
+ <SelectRow onClick={sessionRemove}>
+ <FaUserMinus size="1rem" color="rgb(0, 255, 255)" /> Session
+ Remove
+ </SelectRow>
+ <SelectRow onClick={blacklistAdd}>
+ <FaBan size="1rem" color="rgb(138, 43, 226)" /> Blacklist
+ Add
+ </SelectRow>
+ <SelectRow onClick={blacklistRemove}>
+ <FaUnlockAlt size="1rem" color="rgb(138, 43, 226)" />{' '}
+ Blacklist Remove
+ </SelectRow>
+ <SelectRow onClick={clientRemove}>
+ <FaTrash size="1rem" color="rgb(225, 53, 57)" /> Delete
+ </SelectRow>
+ </SelectContainer>
+ ) : null;
+ }
+}
+
+export default Select;
diff --git a/server/web/src/components/Server.tsx b/server/web/src/components/Server.tsx
new file mode 100644
index 0000000..c581cf6
--- /dev/null
+++ b/server/web/src/components/Server.tsx
@@ -0,0 +1,571 @@
+import { IProps, IState } from '../interfaces/components/Server.interface';
+import { clientsLoad, sessionLoad } from '../redux/actions';
+import { IClient } from '../interfaces/Client.interface';
+import React, { Component, Fragment } from 'react';
+import { connect } from 'react-redux';
+import Sidebar from './Sidebar';
+import Footer from './Footer';
+import Select from './Select';
+import {
+ ServerTable,
+ ServerTableHead,
+ ServerTableBody,
+ ServerTableRow,
+ ServerTableHeader,
+ ServerTableData,
+ ServerTableImage,
+ ServerTableBarBg,
+ ServerTableBar,
+ ServerBlock
+} from '../design/components/Server.design';
+
+class Server extends Component<IProps, IState> {
+ tableBody: any = React.createRef();
+ touchStartMenuTimestamp = 0;
+ touchEndMenuTimestamp = 0;
+ lastSelected: any = null;
+ // CONSTANT : assumes these are
+ // the same on the server side
+ displayKeys = [
+ 'Row',
+ 'Country',
+ 'Connect IP',
+ 'Unique ID',
+ 'Username',
+ 'Hostname',
+ 'Privileges',
+ 'Antivirus',
+ 'Operating System',
+ 'CPU',
+ 'GPU',
+ 'RAM',
+ 'Active Window',
+ 'Idle Time',
+ 'Resource Usage'
+ ]
+ displayValues = [
+ 'username',
+ 'hostname',
+ 'privileges',
+ 'antivirus',
+ 'operating_system',
+ 'cpu',
+ 'gpu',
+ 'ram'
+ ]
+ hiddenKeys = [
+ 'Initial Connect',
+ 'Filepath',
+ 'Running',
+ 'Build Name',
+ 'Build Version',
+ 'OS Version',
+ 'System Locale',
+ 'System Uptime',
+ 'PC Manufacturer',
+ 'PC Model',
+ 'MAC Address',
+ 'External IP',
+ 'Local IP',
+ 'Timezone',
+ 'Country Code',
+ 'Region',
+ '~City',
+ '~Zip Code',
+ '~Latitude',
+ '~Longitude'
+ ]
+ hiddenValues = [
+ 'initial_connect',
+ 'filepath',
+ 'running',
+ 'build_name',
+ 'build_version',
+ 'os_version',
+ 'system_locale',
+ 'system_uptime',
+ 'pc_manufacturer',
+ 'pc_model',
+ 'mac_address',
+ 'external_ip',
+ 'local_ip',
+ 'timezone',
+ 'country_code',
+ 'region',
+ 'city',
+ 'zip_code',
+ 'latitude',
+ 'longitude'
+ ]
+
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ selectData: { show: false },
+ clients: props.clients,
+ session: props.session
+ };
+ }
+
+ componentDidMount() {
+ const { clientsLoad, sessionLoad } = this.props;
+
+ window.addEventListener('click', () => {
+ this.setState({ selectData: { show: false } });
+ this.clearSelected();
+ });
+
+ window.eel.clients_eel()((clients: Map<string, IClient>) =>
+ window.eel.session_eel()((session: Set<string>) => {
+ clientsLoad(clients);
+ sessionLoad(session);
+ })
+ );
+ }
+
+ sessionAdd = () => {
+ const selected = this.allSelected('unique-id');
+ const length = selected.length;
+
+ if (length > 0) {
+ window.eel.execute_eel({
+ message: 'session',
+ id: selected.join(','),
+ })((response: string) => console.log(response));
+ window.showAlert({
+ message: `Client${this.plural(selected)} Added To Session`,
+ type: 'SUCCESS',
+ });
+ } else this.selectMenuError();
+ };
+
+ sessionRemove = () => {
+ const selected = this.allSelected('unique-id');
+ const length = selected.length;
+
+ if (length > 0) {
+ window.eel.execute_eel({
+ message: 'session',
+ id: selected.join(','),
+ remove: true,
+ })((response: string) => console.log(response));
+ window.showAlert({
+ message: `Client${this.plural(selected)} Removed From Session`,
+ type: 'SUCCESS',
+ });
+ } else this.selectMenuError();
+ };
+
+ blacklistAdd = () => {
+ const selected = this.allSelected('connect-ip');
+ const length = selected.length;
+
+ if (length > 0) {
+ window.eel.execute_eel({
+ message: 'blacklist',
+ add: selected.join(','),
+ })((response: string) => console.log(response));
+ window.showAlert({
+ message: `Blacklist Address${this.plural(selected, 'es')} Added`,
+ type: 'SUCCESS',
+ });
+ } else this.selectMenuError();
+ };
+
+ blacklistRemove = () => {
+ const selected = this.allSelected('connect-ip');
+ const length = selected.length;
+
+ if (length > 0) {
+ window.eel.execute_eel({
+ message: 'blacklist',
+ remove: selected.join(','),
+ })((response: string) => console.log(response));
+ window.showAlert({
+ message: `Blacklist Address${this.plural(selected, 'es')} Removed`,
+ type: 'SUCCESS',
+ });
+ } else this.selectMenuError();
+ };
+
+ clientRemove = () => {
+ const selected = this.allSelected('unique-id');
+ const length = selected.length;
+
+ if (length > 0) {
+ window.eel.execute_eel({
+ message: 'delete',
+ id: selected.join(','),
+ })((response: string) => console.log(response));
+ window.showAlert({
+ message: `Client${this.plural(selected)} Removed`,
+ type: 'SUCCESS',
+ });
+ } else this.selectMenuError();
+ };
+
+ clipboard = (data: string) => (event: any) => {
+ if (event.altKey) {
+ if (window.isSecureContext) {
+ window.navigator.clipboard.writeText(data);
+ window.showAlert({
+ message: 'Field Copied To Clipboard',
+ type: 'SUCCESS'
+ });
+ } else
+ window.showAlert({
+ message: 'Clipboard Failed',
+ type: 'DANGER',
+ });
+
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ };
+
+ selectMenuError = () => window.showAlert({
+ message: 'Select Menu Error',
+ type: 'DANGER',
+ });
+
+ plural = (array: string[], end = 's') =>
+ array.length === 1 ? '' : end;
+
+ errorFlag = (event: any) =>
+ // CONSTANT : placeholder flag name
+ event.currentTarget.src = './static/flags/placeholder.png';
+
+ properties = (client: IClient) => {
+ let result: string[] | string = [];
+
+ for (let i = 0; i < this.hiddenValues.length; i++) {
+ const value = (client as any)[this.hiddenValues[i]];
+ const key = this.hiddenKeys[i];
+ result.push(`${key}: ${value}`);
+ }
+
+ return this.propertiesResult(result.join('\n'));
+ };
+
+ propertiesResult = (result: string) => ({
+ onContextMenu: this.clipboard(result),
+ title: result
+ });
+
+ menu = (event: any) =>
+ this.isSelected(event.currentTarget) &&
+ this.setState({
+ selectData: {
+ show: true,
+ // CONSTANT : when to swap menu horizontally (right to left)
+ left:
+ window.innerWidth - event.clientX < 165
+ ? event.clientX - 136
+ : event.clientX,
+ // CONSTANT : when to swap menu vertically (bottom to top)
+ top:
+ window.innerHeight - event.clientY < 165
+ ? event.clientY - 150
+ : event.clientY,
+ sessionAdd: this.sessionAdd,
+ sessionRemove: this.sessionRemove,
+ blacklistAdd: this.blacklistAdd,
+ blacklistRemove: this.blacklistRemove,
+ clientRemove: this.clientRemove
+ },
+ });
+
+ touchStartMenu = (event: any) =>
+ (this.touchStartMenuTimestamp = event.timeStamp);
+
+ touchEndMenu = (event: any) => {
+ this.touchEndMenuTimestamp = event.timeStamp;
+ // CONSTANT : hold down touch time
+ this.touchEndMenuTimestamp - this.touchStartMenuTimestamp > 300 &&
+ this.menu(event);
+ };
+
+ select = (event: any) => {
+ this.setState({ selectData: { show: false } });
+ const current = event.currentTarget;
+
+ if (event.ctrlKey) {
+ if (this.isSelected(current)) {
+ this.removeSelected(current);
+ this.lastSelected = null;
+ } else {
+ this.addSelected(current);
+ this.lastSelected = current;
+ }
+ } else if (event.shiftKey) {
+ const rows = this.tableBody.current.rows;
+ let currentPosition = 0;
+ let latestPosition = 0;
+
+ for (let i = 0; i < rows.length; i++)
+ if (rows[i] === current) currentPosition = i;
+ else if (rows[i] === this.lastSelected) latestPosition = i;
+
+ this.rangeSelect(rows, currentPosition, latestPosition);
+ } else this.singleSelect(current);
+
+ event.stopPropagation();
+ };
+
+ singleSelect = (row: any) => {
+ const rows = this.tableBody.current.rows;
+ let currentSelected = false,
+ otherSelected = false;
+
+ for (let i = 0; i < rows.length; i++)
+ if (rows[i] !== row) {
+ if (this.isSelected(rows[i])) {
+ if (!otherSelected) otherSelected = true;
+ this.removeSelected(rows[i]);
+ }
+ } else if (this.isSelected(row)) currentSelected = true;
+
+ if (currentSelected && !otherSelected) {
+ this.removeSelected(row);
+ this.lastSelected = null;
+ } else {
+ this.addSelected(row);
+ this.lastSelected = row;
+ }
+ };
+
+ rangeSelect = (rows: any, start: number, end: number) => {
+ if (this.lastSelected !== rows[start]) {
+ if (start > end) {
+ let n = start;
+ start = end;
+ end = n;
+ }
+
+ for (let i = 0; i < rows.length; i++)
+ if (i >= start && i <= end) {
+ if (!this.isSelected(rows[i])) this.addSelected(rows[i]);
+ } else if (this.isSelected(rows[i])) this.removeSelected(rows[i]);
+ } else this.singleSelect(this.lastSelected);
+ };
+
+ allSelected = (dataAttribute: string) => {
+ let result = [];
+
+ if (this.tableBody.current !== null) {
+ const rows = this.tableBody.current.rows;
+
+ for (let i = 0; i < rows.length; i++)
+ if (this.isSelected(rows[i]))
+ result.push(rows[i].getAttribute(`data-${dataAttribute}`));
+ }
+
+ return result;
+ };
+
+ clearSelected = () => {
+ if (this.tableBody.current !== null) {
+ const rows = this.tableBody.current.rows;
+ this.lastSelected = null;
+
+ for (let i = 0; i < rows.length; i++)
+ if (this.isSelected(rows[i])) this.removeSelected(rows[i]);
+ }
+ };
+
+ addSelected = (row: any) => {
+ row.setAttribute('data-selected', '');
+ row.style.backgroundColor = 'rgb(0, 40, 80)';
+ };
+
+ removeSelected = (row: any) => {
+ row.removeAttribute('data-selected');
+ row.removeAttribute('style');
+ };
+
+ isSelected = (row: any) => row.hasAttribute('data-selected');
+
+ render() {
+ const { clients, session } = this.props;
+ const { selectData } = this.state;
+
+ return (
+ <Fragment>
+ {clients.size > 0 ? (
+ <ServerTable>
+ <ServerTableHead>
+ <ServerTableRow>
+ {this.displayKeys.map((category: string, index: number) => (
+ <ServerTableHeader key={index} title={category}>
+ {category}
+ </ServerTableHeader>
+ ))}
+ </ServerTableRow>
+ </ServerTableHead>
+ <ServerTableBody ref={this.tableBody}>
+ {Array.from(clients.entries()).map(([unique_id, client]: [string, IClient], row: number) => (
+ <ServerTableRow
+ key={row}
+ data-unique-id={unique_id}
+ data-connect-ip={client.connect_ip}
+ onClick={this.select}
+ onContextMenu={this.menu}
+ onTouchStart={this.touchStartMenu}
+ onTouchEnd={this.touchEndMenu}
+ activeSession={
+ session.has(unique_id)
+ ? 'rgb(0, 255, 255)'
+ : 'rgb(255, 255, 255)'
+ }
+ >
+ <ServerTableData
+ data-label={this.displayKeys[0]}
+ {...this.properties(client)}
+ >
+ {row + 1}
+ </ServerTableData>
+
+ <ServerTableData
+ data-label={this.displayKeys[1]}
+ title={client.country}
+ onContextMenu={this.clipboard(client.country)}
+ >
+ <Fragment>
+ <ServerTableImage
+ src={`./static/flags/${client.country_code}.png`}
+ onError={this.errorFlag}
+ />
+ {client.country}
+ </Fragment>
+ </ServerTableData>
+
+ <ServerTableData
+ data-label={this.displayKeys[2]}
+ title={client.connect_ip}
+ onContextMenu={this.clipboard(client.connect_ip)}
+ >
+ {client.connect_ip}
+ </ServerTableData>
+
+ <ServerTableData
+ data-label={this.displayKeys[3]}
+ title={unique_id}
+ onContextMenu={this.clipboard(unique_id)}
+ >
+ {unique_id}
+ </ServerTableData>
+
+ {this.displayValues.map(
+ (displayValue: string, column: number) => (
+ <ServerTableData
+ key={column}
+ data-label={
+ // CONSTANT : start index
+ this.displayKeys[4 + column]
+ }
+ title={(client as any)[displayValue]}
+ onContextMenu={this.clipboard(
+ (client as any)[displayValue]
+ )}
+ >
+ {(client as any)[displayValue]}
+ </ServerTableData>
+ )
+ )}
+
+ <ServerTableData
+ data-label={
+ this.displayKeys[this.displayKeys.length - 3]
+ }
+ {...client.active_window && (
+ {
+ title: client.active_window,
+ onContextMenu: this.clipboard(
+ client.active_window
+ )
+ }
+ )}
+ >
+ {client.active_window ? client.active_window : '...'}
+ </ServerTableData>
+
+ <ServerTableData
+ data-label={
+ this.displayKeys[this.displayKeys.length - 2]
+ }
+ {...client.idle_time && (
+ {
+ title: client.idle_time,
+ onContextMenu: this.clipboard(
+ client.idle_time
+ )
+ }
+ )}
+ >
+ {client.idle_time ? client.idle_time : '...'}
+ </ServerTableData>
+
+ <ServerTableData
+ data-label={
+ this.displayKeys[this.displayKeys.length - 1]
+ }
+ {...client.resource_usage && (
+ {
+ title: client.resource_usage,
+ onContextMenu: this.clipboard(
+ client.resource_usage
+ )
+ }
+ )}
+ >
+ {client.resource_usage ? (
+ // CONSTANT : cpu/ram
+ client.resource_usage
+ .split('/')
+ .map((bar: string) => (
+ <ServerTableBarBg>
+ <ServerTableBar width={bar} />
+ </ServerTableBarBg>
+ ))
+ ) : '...'}
+ </ServerTableData>
+ </ServerTableRow>
+ ))}
+ </ServerTableBody>
+ </ServerTable>
+ ) : (
+ <ServerBlock>No Clients Connected</ServerBlock>
+ )}
+ <Sidebar />
+ <Footer />
+ <Select
+ show={selectData.show}
+ top={selectData.top}
+ left={selectData.left}
+ sessionAdd={selectData.sessionAdd}
+ sessionRemove={selectData.sessionRemove}
+ blacklistAdd={selectData.blacklistAdd}
+ blacklistRemove={selectData.blacklistRemove}
+ clientRemove={selectData.clientRemove}
+ />
+ </Fragment>
+ );
+ }
+}
+
+const mapStateToProps = (state: IProps) => {
+ return {
+ session: state.session,
+ clients: state.clients
+ };
+};
+
+const mapDispatchToProps = (dispatch: any) => {
+ return {
+ clientsLoad: (clients: Map<string, IClient>) => dispatch(clientsLoad(clients)),
+ sessionLoad: (session: Set<string>) => dispatch(sessionLoad(session))
+ };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(Server);
diff --git a/server/web/src/components/Sidebar.tsx b/server/web/src/components/Sidebar.tsx
new file mode 100644
index 0000000..d016a19
--- /dev/null
+++ b/server/web/src/components/Sidebar.tsx
@@ -0,0 +1,136 @@
+import { IProps, IState } from '../interfaces/components/Sidebar.interface';
+import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
+import { IStream } from '../interfaces/Stream.interface';
+import React, { Component } from 'react';
+import Card from './Card';
+import {
+ SidebarDropdown,
+ SidebarDropdownButton,
+ SidebarDropdownContent,
+ SidebarStreamContent,
+ SidebarStream,
+ SidebarStreamSection,
+ SidebarStreamInput,
+ SidebarStreamButton,
+ SidebarBlock
+} from '../design/components/Sidebar.design';
+
+class Sidebar extends Component<IProps, IState> {
+ sourceInput: any = React.createRef();
+ titleInput: any = React.createRef();
+
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ showContent: false,
+ streams: []
+ };
+ }
+
+ removeStream = (toRemove: IStream) =>
+ this.setState({
+ streams: this.state.streams.filter(
+ (stream: IStream) => (
+ !(stream.source === toRemove.source
+ && stream.title === toRemove.title)
+ )
+ )
+ });
+
+ addStream = () => {
+ const source = this.sourceInput.current;
+ const title = this.titleInput.current;
+ const sourceValue = source.value.trim();
+ const titleValue = title.value.trim();
+ let sourcePassed = true;
+ let titlePassed = true;
+
+ if (sourceValue === '') {
+ source.style.border = 'solid thin rgb(225, 53, 57)';
+ sourcePassed = false;
+ } else
+ source.style.border = 'solid thin rgb(31, 93, 117)';
+
+ if (titleValue === '') {
+ title.style.border = 'solid thin rgb(225, 53, 57)';
+ titlePassed = false;
+ } else
+ title.style.border = 'solid thin rgb(31, 93, 117)';
+
+ if (sourcePassed && titlePassed)
+ if (this.streamExists(sourceValue, titleValue))
+ alert('Stream Already Exists!')
+ else {
+ this.state.streams.push({
+ source: sourceValue,
+ title: titleValue
+ });
+ source.value = '';
+ title.value = '';
+ }
+ }
+
+ streamExists = (source: string, title: string) => {
+ for (const stream of this.state.streams)
+ if (stream.source === source && stream.title === title)
+ return true;
+ return false;
+ }
+
+ updateShowContent = () =>
+ this.setState({ showContent: !this.state.showContent });
+
+ render() {
+ const { showContent, streams } = this.state;
+
+ return (
+ <SidebarDropdown>
+ <SidebarDropdownButton onClick={this.updateShowContent}>
+ {showContent ? (
+ <FaChevronRight size=".8rem" />
+ ) : (
+ <FaChevronLeft size=".8rem" />
+ )}
+ </SidebarDropdownButton>
+
+ <SidebarDropdownContent active={showContent}>
+ <SidebarStream>
+ <SidebarStreamInput
+ type="search"
+ placeholder="Flash video source..."
+ ref={this.sourceInput}
+ />
+ <SidebarStreamSection>
+ <SidebarStreamInput
+ type="search"
+ placeholder="Stream title..."
+ ref={this.titleInput}
+ />
+ <SidebarStreamButton
+ onClick={this.addStream}
+ >
+ Add
+ </SidebarStreamButton>
+ </SidebarStreamSection>
+ </SidebarStream>
+ {streams.length === 0 ? (
+ <SidebarBlock>No Streams Available</SidebarBlock>
+ ) : (
+ <SidebarStreamContent>
+ {streams.map((stream: IStream) => (
+ <Card
+ removeStream={this.removeStream}
+ source={stream.source}
+ title={stream.title}
+ />
+ ))}
+ </SidebarStreamContent>
+ )}
+ </SidebarDropdownContent>
+ </SidebarDropdown>
+ );
+ }
+}
+
+export default Sidebar;
diff --git a/server/web/src/components/Window.tsx b/server/web/src/components/Window.tsx
new file mode 100644
index 0000000..2677819
--- /dev/null
+++ b/server/web/src/components/Window.tsx
@@ -0,0 +1,308 @@
+import { IProps, IState } from '../interfaces/components/Window.interface';
+import React, { Component, Fragment } from 'react';
+import {
+ WindowData,
+ WindowTopBar,
+ WindowTopBarTitle,
+ WindowTopBarAction,
+ WindowContent,
+ WindowResult,
+ WindowForm,
+ WindowInputGroup,
+ WindowLabel,
+ WindowInput,
+ WindowCheckbox,
+ WindowFormSubmit,
+ WindowFormButton,
+ WindowFormClear
+} from '../design/components/Window.design';
+import {
+ FaMinusSquare,
+ FaWindowMaximize,
+ FaWindowMinimize,
+ FaWindowClose,
+ FaRedo
+} from 'react-icons/fa';
+
+class Window extends Component<IProps, IState> {
+ windowResult: any = React.createRef();
+ windowTitle: any = React.createRef();
+ windowForm: any = React.createRef();
+ window: any = React.createRef();
+
+ constructor(props: IProps) {
+ super(props);
+ this.state = {
+ result: props.data || [],
+ fullscreen: false,
+ dragging: false,
+ pos: props.pos,
+ rel: null
+ };
+ }
+
+ componentDidMount() {
+ document.addEventListener('fullscreenchange', this.fullscreenEvent);
+
+ if (this.windowForm.current === null)
+ // CONSTANT : window height minus the margin & borders
+ this.windowResult.current.style.height = 'calc(100% - 1.625rem)';
+ else
+ // CONSTANT : window height minus the form height, margin & borders
+ // CONSTANT : Rem unit dependant & px scrollHeight
+ this.windowResult.current.style.height = `
+ calc(100% - 1.625rem - ${this.windowForm.current.scrollHeight / 16}rem)
+ `;
+ }
+
+ componentDidUpdate(_: IProps, state: IState) {
+ const { dragging } = this.state;
+
+ if (dragging && !state.dragging) {
+ document.addEventListener('mousemove', this.windowMove);
+ document.addEventListener('mouseup', this.windowDrop);
+ } else if (!dragging && state.dragging) {
+ document.removeEventListener('mousemove', this.windowMove);
+ document.removeEventListener('mouseup', this.windowDrop);
+ }
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('fullscreenchange', this.fullscreenEvent);
+ }
+
+ windowMove = (event: any) => {
+ const { dragging, rel } = this.state;
+
+ if (!dragging) return;
+
+ this.setState({
+ pos: {
+ x: event.pageX - rel.x,
+ y: event.pageY - rel.y
+ }
+ });
+ };
+
+ windowGrab = (event: any) => {
+ if (event.button !== 0) return;
+
+ this.windowTitle.current.style.cursor = 'grabbing';
+ this.props.hightlight();
+
+ this.setState({
+ dragging: true,
+ rel: {
+ x: event.pageX - this.window.current.offsetLeft,
+ y: event.pageY - this.window.current.offsetTop
+ }
+ });
+ };
+
+ windowDrop = () => {
+ this.windowTitle.current.style.cursor = 'grab';
+ this.setState({ dragging: false });
+ };
+
+ clearResult = (event: any) => {
+ this.setState({
+ result: this.state.result.filter(
+ (item: string) => item === 'Request Sent... Awaiting Response')
+ });
+ event.preventDefault();
+ };
+
+ executeRequest = (event: any) => {
+ const { requestType, requestArgs } = this.props;
+ let windowResult: any = {
+ message: requestType.toLowerCase()
+ };
+ let requiredFields = true;
+
+ for (let i = 0; i < requestArgs.length; i++)
+ if (requestArgs[i][2])
+ if (
+ requestArgs[i][1] &&
+ !event.target[requestArgs[i][0]].value
+ ) {
+ event.target[requestArgs[i][0]].style.border =
+ 'solid 0.0625rem rgb(225, 53, 57)';
+ requiredFields = false;
+ } else {
+ event.target[requestArgs[i][0]].style.border =
+ 'solid 0.0625rem rgb(31, 93, 117)';
+ windowResult[requestArgs[i][0]] =
+ event.target[requestArgs[i][0]].value;
+ }
+ else
+ windowResult[requestArgs[i][0]] =
+ event.target[requestArgs[i][0]].checked;
+
+ if (requiredFields)
+ this.setState({
+ result: ['Request Sent... Awaiting Response', ...this.state.result]
+ }, () =>
+ window.eel.execute_eel(windowResult)((response: string) => {
+ const result = this.state.result.reverse()
+ let awaitResponseMessage = true;
+
+ this.setState({
+ result: result.map(
+ (item: string) => {
+ if (item === 'Request Sent... Awaiting Response')
+ if (awaitResponseMessage) {
+ awaitResponseMessage = false;
+ return response;
+ } else
+ return item;
+ else
+ return item;
+ }
+ ).reverse()
+ })
+ })
+ )
+
+ event.preventDefault();
+ };
+
+ updateCheckbox = (event: any) => {
+ if (event.target.previousSibling.hasAttribute('checked'))
+ event.target.previousSibling.removeAttribute('checked');
+ else event.target.previousSibling.setAttribute('checked', '');
+ };
+
+ fullscreenEvent = () => {
+ if (document.fullscreenElement) this.setState({ fullscreen: true });
+ else this.setState({ fullscreen: false });
+ };
+
+ enterFullscreen = () => this.window.current.requestFullscreen();
+
+ exitFullscreen = () => document.exitFullscreen();
+
+ render() {
+ const { pos, result, fullscreen } = this.state;
+ const {
+ requestType,
+ requestArgs,
+ hightlight,
+ toggle,
+ destroy
+ } = this.props;
+
+ return (
+ <WindowData
+ ref={this.window}
+ style={{
+ left: `${pos.x}px`,
+ top: `${pos.y}px`
+ }}
+ >
+ <WindowTopBar>
+ <WindowTopBarTitle
+ title={`${requestType} Request / Response Window`}
+ onMouseDown={this.windowGrab}
+ ref={this.windowTitle}
+ >
+ {requestType}
+ </WindowTopBarTitle>
+ <WindowTopBarAction>
+ {fullscreen ? (
+ <FaWindowMinimize
+ onClick={this.exitFullscreen}
+ size="1rem"
+ />
+ ) : (
+ <FaWindowMaximize
+ onClick={this.enterFullscreen}
+ size="1rem"
+ />
+ )}{' '}
+ <FaMinusSquare onClick={toggle} size="1rem" />{' '}
+ <FaWindowClose onClick={destroy} size="1rem" />
+ </WindowTopBarAction>
+ </WindowTopBar>
+ <WindowContent onMouseDown={hightlight}>
+ <WindowResult ref={this.windowResult}>
+ {result.length > 0 ? (
+ result.map((response: any, index: number) => (
+ <Fragment key={index}>
+ {response ? (
+ response.html ? (
+ <div
+ dangerouslySetInnerHTML={{
+ __html: response.message
+ ? response.message
+ : 'Empty Response'
+ }}
+ ></div>
+ ) : (
+ <div>{response}</div>
+ )
+ ) : (
+ <div>Empty Response</div>
+ )}
+ </Fragment>
+ ))
+ ) : (
+ <div>No Responses Present</div>
+ )}
+ </WindowResult>
+ {requestArgs.length > 0 ? (
+ <WindowForm
+ onSubmit={this.executeRequest}
+ ref={this.windowForm}
+ >
+ {requestArgs.map((
+ [name, required, response]: [string, boolean, boolean],
+ index: number
+ ) =>
+ response ? (
+ <WindowInputGroup key={index}>
+ <WindowLabel>
+ {required
+ ? 'Required'
+ : 'Optional'}
+ </WindowLabel>
+
+ <WindowInput
+ type="search"
+ name={name}
+ placeholder={name}
+ />
+ </WindowInputGroup>
+ ) : (
+ <WindowInputGroup key={index}>
+ <WindowLabel>{name}</WindowLabel>
+ <WindowCheckbox>
+ <input
+ type="checkbox"
+ name={name}
+ />
+ <label
+ onClick={
+ this.updateCheckbox
+ }
+ ></label>
+ </WindowCheckbox>
+ </WindowInputGroup>
+ )
+ )}
+ <WindowFormSubmit>
+ <WindowFormButton>
+ Execute Request
+ </WindowFormButton>
+ <WindowFormClear onClick={this.clearResult}>
+ <FaRedo />
+ </WindowFormClear>
+ </WindowFormSubmit>
+ </WindowForm>
+ ) : null}
+ </WindowContent>
+ </WindowData>
+ );
+ }
+}
+
+export default Window;
diff --git a/server/web/src/design/GlobalStyle.tsx b/server/web/src/design/GlobalStyle.tsx
new file mode 100644
index 0000000..25c47bf
--- /dev/null
+++ b/server/web/src/design/GlobalStyle.tsx
@@ -0,0 +1,52 @@
+import { createGlobalStyle } from 'styled-components';
+
+// NOTE : rem unit is used instead of px. But
+// the UI is still dependant on px units, mostly
+// because of some javascript calculations
+export const GlobalStyle = createGlobalStyle`
+ ::-webkit-scrollbar {
+ height: 0.2rem;
+ width: 0.4rem;
+ }
+
+ ::-webkit-scrollbar-track {
+ background-color: rgb(5, 32, 58);
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background-color: rgb(31, 113, 145);
+ }
+
+ ::placeholder {
+ text-transform: capitalize;
+ color: rgb(204, 204, 204);
+ opacity: 1;
+ }
+
+ * {
+ box-sizing: border-box;
+ outline: none;
+ }
+
+ body {
+ font-family: Arial, Helvetica, sans-serif;
+ background-color: rgb(5, 32, 58);
+ color: rgb(255, 255, 255);
+ font-size: 0.7rem;
+ font-weight: 100;
+ margin: 0;
+ }
+
+ button, svg {
+ cursor: pointer;
+ }
+
+ button {
+ transition: 150ms;
+
+ :active {
+ transform: translateY(0.125rem);
+ transition: 150ms;
+ }
+ }
+`;
diff --git a/server/web/src/design/components/Alert.design.tsx b/server/web/src/design/components/Alert.design.tsx
new file mode 100644
index 0000000..ff634bf
--- /dev/null
+++ b/server/web/src/design/components/Alert.design.tsx
@@ -0,0 +1,48 @@
+import { IAlertButton } from '../../interfaces/design/AlertDesign.interface';
+import styled from 'styled-components';
+
+const calcBgColor = (color: string) => {
+ switch (color) {
+ case 'SUCCESS':
+ return 'rgb(24, 79, 59)';
+ case 'DANGER':
+ return 'rgb(175, 45, 45)';
+ case 'WARNING':
+ return 'rgb(175, 121, 21)';
+ case 'INFO':
+ return 'rgb(23, 52, 102)';
+ }
+};
+
+export const AlertButton = styled('div') <IAlertButton>`
+ background-color: ${(props) => calcBgColor(props.bgColor)};
+ border: solid thin rgb(255, 255, 255);
+ grid-template-columns: 1fr auto;
+ color: rgb(255, 255, 255);
+ align-items: center;
+ min-height: 2.5rem;
+ text-align: center;
+ font-size: 0.8rem;
+ width: 17.5rem;
+ display: grid;
+
+ @media (min-width: 768px) {
+ width: 22.5rem;
+ }
+`;
+
+export const AlertButtonCross = styled('span')`
+ border-left: solid thin rgb(255, 255, 255);
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ padding: 0.5rem 1rem;
+ align-items: center;
+ font-size: 1.25rem;
+ user-select: none;
+ cursor: pointer;
+ display: grid;
+ height: 100%;
+`;
diff --git a/server/web/src/design/components/Card.design.tsx b/server/web/src/design/components/Card.design.tsx
new file mode 100644
index 0000000..2eb67a3
--- /dev/null
+++ b/server/web/src/design/components/Card.design.tsx
@@ -0,0 +1,60 @@
+import styled from 'styled-components';
+
+export const CardContainer = styled('div')`
+ border: solid thin rgb(31, 113, 145);
+ background-color: rgb(5, 32, 58);
+ // CONSTANT : left & right borders + left & right margin
+ width: calc(100% - 1.125rem);
+ color: rgb(255, 255, 255);
+ border-radius: 0.5rem;
+ display: inline-block;
+ margin: 0.5rem;
+
+ @media (min-width: 992px) {
+ width: 46.5%;
+ }
+`;
+
+export const CardHeader = styled('div')`
+ border-bottom: solid thin rgb(31, 113, 145);
+ padding: 0.5rem 0.5rem 0.625rem 0.5rem;
+ border-top-right-radius: 0.5rem;
+ border-top-left-radius: 0.5rem;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 0.9rem;
+ text-align: left;
+ overflow: hidden;
+`;
+
+export const CardFooter = styled('div')`
+ border-top: solid thin rgb(31, 113, 145);
+ border-bottom-right-radius: 0.5rem;
+ border-bottom-left-radius: 0.5rem;
+ grid-template-columns: 1fr 1fr;
+ display: grid;
+`;
+
+export const CardFooterItem = styled('div')`
+ border-right: solid thin rgb(31, 113, 145);
+ justify-content: center;
+ align-items: center;
+ font-size: 0.65rem;
+ padding: 0.5rem 0;
+ cursor: pointer;
+ display: grid;
+
+ :hover {
+ background-color: rgb(0, 55, 117);
+ transition: 250ms;
+ }
+
+ :first-child {
+ border-bottom-left-radius: 0.5rem;
+ }
+
+ :last-child {
+ border-bottom-right-radius: 0.5rem;
+ border-right: none;
+ }
+`;
diff --git a/server/web/src/design/components/Footer.design.tsx b/server/web/src/design/components/Footer.design.tsx
new file mode 100644
index 0000000..a5cf586
--- /dev/null
+++ b/server/web/src/design/components/Footer.design.tsx
@@ -0,0 +1,132 @@
+import { IFooterDropdownContent } from '../../interfaces/design/FooterDesign.interface';
+import styled from 'styled-components';
+
+export const FooterDropdown = styled('div')`
+ border-top: solid thin rgb(31, 93, 117);
+ background-color: rgb(5, 32, 58);
+ width: calc(100% - 1.2rem);
+ text-align: center;
+ position: fixed;
+ bottom: 4.45rem;
+ left: 0;
+`;
+
+export const FooterDropdownToggle = styled('div')`
+ background-color: rgb(5, 32, 58);
+ color: rgb(0, 255, 255);
+ padding: 0.25rem 0;
+ cursor: pointer;
+`;
+
+export const FooterWindowManager = styled('div')`
+ border-top: solid thin rgb(31, 93, 117);
+ background-color: rgb(5, 32, 58);
+ width: calc(100% - 1.2rem);
+ color: rgb(255, 255, 255);
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ white-space: nowrap;
+ text-align: center;
+ overflow-x: scroll;
+ overflow-y: hidden;
+ position: fixed;
+ height: 3.25rem;
+ bottom: 1.2rem;
+ left: 0;
+`;
+
+export const FooterWindowButton = styled('button')`
+ border: solid thin rgb(31, 93, 117);
+ background-color: rgb(5, 32, 58);
+ margin: 0.578125rem 0.15rem;
+ color: rgb(255, 255, 255);
+ padding: 0.5rem 1.5rem;
+ border-radius: 5rem;
+ transition: 250ms;
+`;
+
+export const FooterWindowClear = styled('button')`
+ border: solid thin rgb(225, 53, 57);
+ background-color: rgb(5, 32, 58);
+ margin: 0.578125rem 0.15rem;
+ color: rgb(255, 255, 255);
+ padding: 0.5rem 1.5rem;
+ border-radius: 5rem;
+ transition: 250ms;
+`;
+
+export const FooterBlock = styled('div')`
+ background-color: rgb(5, 32, 58);
+ color: rgb(255, 255, 255);
+ margin: 1.1875rem 0;
+`;
+
+export const FooterDropdownContent = styled('div') <IFooterDropdownContent>`
+ background-color: rgb(5, 32, 58);
+ overflow-y: scroll;
+ transition: 250ms;
+ padding: 0;
+ height: 0;
+
+ ${({ active }: any) =>
+ active &&
+ `
+ border-top: solid thin rgb(31, 93, 117);
+ padding: 0.5rem 0;
+ transition: 250ms;
+ height: 9.2rem;
+
+ @media (min-width: 1280px) {
+ height: 7.2rem;
+ }
+
+ @media (min-width: 2000px) {
+ height: 5.2rem;
+ }
+ `}
+`;
+
+export const FooterNameSpaceButton = styled('button')`
+ border: solid thin rgb(225, 53, 57);
+ background-color: rgb(225, 53, 57);
+ color: rgb(255, 255, 255);
+ padding: 0.4rem 1rem;
+ border-radius: 5rem;
+ transition: none;
+ cursor: text;
+
+ :active {
+ transform: none;
+ transition: none;
+ }
+`;
+
+export const FooterDropdownButton = styled('button')`
+ background-color: rgb(31, 93, 117);
+ color: rgb(255, 255, 255);
+ border: rgb(31, 93, 117);
+ padding: 0.4rem 1rem;
+ border-radius: 5rem;
+ margin: 0.15rem;
+`;
+
+export const FooterMenu = styled('div')`
+ border-top: solid thin rgb(31, 93, 117);
+ background-color: rgb(5, 32, 58);
+ width: calc(100% - 1.2rem);
+ color: rgb(255, 255, 255);
+ position: fixed;
+ height: 1.2rem;
+ bottom: 0;
+ left: 0;
+`;
+
+export const FooterParagraph = styled('p')`
+ text-overflow: ellipsis;
+ padding-top: 0.15rem;
+ white-space: nowrap;
+ font-size: 0.75rem;
+ overflow: hidden;
+ height: 1.2rem;
+ margin-top: 0;
+`;
diff --git a/server/web/src/design/components/Select.design.tsx b/server/web/src/design/components/Select.design.tsx
new file mode 100644
index 0000000..92e6719
--- /dev/null
+++ b/server/web/src/design/components/Select.design.tsx
@@ -0,0 +1,37 @@
+import styled from 'styled-components';
+
+export const SelectContainer = styled('div')`
+ position: fixed;
+`;
+
+export const SelectRow = styled('div')`
+ border-bottom: solid thin rgb(31, 113, 145);
+ border-right: solid thin rgb(31, 113, 145);
+ border-left: solid thin rgb(31, 113, 145);
+ background-color: rgb(5, 32, 58);
+ font-size: 0.8rem;
+ transition: 250ms;
+ padding: 0.4rem;
+ cursor: pointer;
+ width: 8.5rem;
+
+ :hover {
+ background-color: rgb(0, 55, 117);
+ transition: 250ms;
+ }
+
+ :first-child {
+ border-top: solid thin rgb(31, 113, 145);
+ border-top-right-radius: 0.25rem;
+ border-top-left-radius: 0.25rem;
+ }
+
+ :last-child {
+ border-bottom-right-radius: 0.25rem;
+ border-bottom-left-radius: 0.25rem;
+ }
+
+ > svg {
+ vertical-align: text-bottom;
+ }
+`;
diff --git a/server/web/src/design/components/Server.design.tsx b/server/web/src/design/components/Server.design.tsx
new file mode 100644
index 0000000..b6296b6
--- /dev/null
+++ b/server/web/src/design/components/Server.design.tsx
@@ -0,0 +1,120 @@
+import styled, { css } from 'styled-components';
+import {
+ IServerTableRow,
+ IServerTableBar
+} from '../../interfaces/design/ServerDesign.interface';
+
+const tableColumn = css`
+ border: solid thin rgb(31, 113, 145);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 3.1rem;
+ border-top: none;
+ overflow: hidden;
+ padding: 0.3rem;
+`;
+
+const tableResponsive = css`
+ @media (max-width: 767px) {
+ display: block;
+ }
+`;
+
+export const ServerTable = styled('table')`
+ width: calc(100% - 1.125rem);
+ color: rgb(255, 255, 255);
+ border-collapse: collapse;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ margin-bottom: 5.9rem;
+ -ms-user-select: none;
+ text-align: center;
+ user-select: none;
+ ${tableResponsive}
+`;
+
+export const ServerTableHead = styled('thead')`
+ background-color: rgb(31, 93, 117);
+
+ @media (max-width: 767px) {
+ display: none;
+ }
+`;
+
+export const ServerTableBody = styled('tbody')`
+ background-color: rgb(5, 32, 58);
+ ${tableResponsive}
+
+ > tr:first-child {
+ padding-top: 1.5rem;
+ }
+
+ > tr:last-child {
+ padding-bottom: 1.5rem;
+ }
+`;
+
+export const ServerTableRow = styled('tr') <IServerTableRow>`
+ ${(props) => props.activeSession && `color: ${props.activeSession};`}
+ padding: 0.75rem 1.5rem;
+ ${tableResponsive}
+
+ > td:first-child {
+ border-top: solid thin rgb(31, 113, 145);
+ }
+`;
+
+export const ServerTableHeader = styled('th')`
+ ${tableColumn}
+`;
+
+export const ServerTableData = styled('td')`
+ ${tableColumn}
+
+ @media (max-width: 767px) {
+ min-height: 1.4125rem;
+ padding-right: 1rem;
+ position: relative;
+ text-align: right;
+ padding-left: 45%;
+ overflow: hidden;
+ max-width: 100%;
+ display: block;
+
+ &:before {
+ content: attr(data-label);
+ position: absolute;
+ padding-left: 1rem;
+ text-align: left;
+ width: 55%;
+ left: 0;
+ }
+ }
+`;
+
+export const ServerTableImage = styled('img')`
+ vertical-align: text-bottom;
+ margin-right: 0.15rem;
+ height: 0.90625rem;
+ width: 0.90625rem;
+`;
+
+export const ServerTableBarBg = styled('div')`
+ background-color: rgb(31, 93, 117);
+`;
+
+export const ServerTableBar = styled('div') <IServerTableBar>`
+ ${(props) => props.width && `width: ${props.width}%;`}
+ background-color: rgb(24, 79, 59);
+ margin: 0.0625rem 0;
+ height: 0.375rem;
+`;
+
+export const ServerBlock = styled('div')`
+ background-color: rgb(31, 93, 117);
+ width: calc(100% - 1.2rem);
+ color: rgb(255, 255, 255);
+ text-align: center;
+ padding: 0.8rem 0;
+`;
diff --git a/server/web/src/design/components/Sidebar.design.tsx b/server/web/src/design/components/Sidebar.design.tsx
new file mode 100644
index 0000000..4496e79
--- /dev/null
+++ b/server/web/src/design/components/Sidebar.design.tsx
@@ -0,0 +1,108 @@
+import { ISidebarSlide } from '../../interfaces/design/SidebarDesign.interface';
+import styled from 'styled-components';
+
+export const SidebarDropdown = styled('div')`
+ background-color: rgb(5, 32, 58);
+ grid-template-columns: 1fr auto;
+ color: rgb(0, 255, 255);
+ position: fixed;
+ display: grid;
+ height: 100%;
+ z-index: 999;
+ right: 0;
+ top: 0;
+`;
+
+export const SidebarDropdownButton = styled('div')`
+ border-left: solid thin rgb(31, 113, 145);
+ padding: 0 0.25rem 0 0.2rem;
+ color: rgb(0, 255, 255);
+ align-items: center;
+ cursor: pointer;
+ width: 1.2rem;
+ display: grid;
+ height: 100vh;
+ z-index: 999;
+`;
+
+export const SidebarDropdownContent = styled('div') <ISidebarSlide>`
+ overflow-x: hidden;
+ text-align: center;
+ transition: 250ms;
+ overflow-y: auto;
+ opacity: 0;
+ padding: 0;
+ width: 0;
+
+ ${({ active }: any) =>
+ active &&
+ `
+ border-left: solid thin rgb(31, 113, 145);
+ width: calc(100vw - 1.2rem);
+ opacity: 1;
+
+ @media (min-width: 576px) {
+ width: 65vw;
+ }
+ `}
+`;
+
+export const SidebarStreamContent = styled('div')`
+ @media (min-width: 992px) {
+ padding: 0.5rem 0;
+ }
+
+ @media (min-width: 1200px) {
+ padding: 1rem 0;
+ }
+
+ @media (min-width: 1600px) {
+ padding: 1.5rem 0;
+ }
+`;
+
+export const SidebarStream = styled('div')`
+ border-bottom: solid thin rgb(31, 113, 145);
+ border-top: solid thin rgb(31, 113, 145);
+ background-color: rgb(5, 32, 58);
+ color: rgb(255, 255, 255);
+ text-align: center;
+ padding: 0.8rem 0;
+`;
+
+export const SidebarStreamSection = styled('div')`
+ grid-template-columns: 1fr auto;
+ display: grid;
+`;
+
+export const SidebarStreamInput = styled('input')`
+ padding: 0.45rem 0.45rem 0.45rem 0.6rem;
+ border: solid thin rgb(31, 93, 117);
+ background-color: rgb(5, 32, 58);
+ width: calc(100% - 0.8rem);
+ color: rgb(255, 255, 255);
+ border-radius: 5rem;
+ font-size: 0.8rem;
+ margin: 0.4rem;
+`;
+
+export const SidebarStreamButton = styled('button')`
+ border: solid thin rgb(31, 93, 117);
+ background-color: rgb(5, 32, 58);
+ margin: 0.4rem 0.4rem 0.4rem 0;
+ color: rgb(255, 255, 255);
+ padding: 0.5rem 2rem;
+ border-radius: 5rem;
+ transition: 250ms;
+
+ @media (min-width: 576px) {
+ padding: 0.5rem 4rem;
+ }
+`;
+
+export const SidebarBlock = styled('div')`
+ background-color: rgb(31, 93, 117);
+ color: rgb(255, 255, 255);
+ text-align: center;
+ padding: 0.8rem 0;
+`;
diff --git a/server/web/src/design/components/Window.design.tsx b/server/web/src/design/components/Window.design.tsx
new file mode 100644
index 0000000..1daac95
--- /dev/null
+++ b/server/web/src/design/components/Window.design.tsx
@@ -0,0 +1,209 @@
+import styled from 'styled-components';
+
+export const WindowData = styled('div')`
+ border: solid 0.0625rem rgb(178, 178, 178);
+ background-color: rgb(5, 32, 58);
+ color: rgb(255, 255, 255);
+ min-height: 2.1875rem;
+ min-width: 18rem;
+ overflow: hidden;
+ position: fixed;
+ resize: both;
+ height: 55vh;
+ width: 40vw;
+
+ &:after {
+ cursor: nwse-resize;
+ position: absolute;
+ display: block;
+ height: 1rem;
+ width: 1rem;
+ content: '';
+ bottom: 0;
+ right: 0;
+ }
+`;
+
+export const WindowTopBar = styled('div')`
+ border-bottom: solid 0.0625rem rgb(31, 93, 117);
+ background-color: rgb(225, 53, 57);
+ grid-template-columns: 1fr auto;
+ align-items: center;
+ text-align: right;
+ display: grid;
+`;
+
+export const WindowTopBarTitle = styled('div')`
+ text-overflow: ellipsis;
+ text-align: left;
+ overflow: hidden;
+ padding: 0.5rem;
+ cursor: grab;
+`;
+
+export const WindowTopBarAction = styled('div')`
+ padding: 0.5rem;
+`;
+
+export const WindowInputGroup = styled('div')`
+ padding: 0 0.75rem 1rem 0.75rem;
+ grid-template-columns: auto 1fr;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ grid-gap: 0.5rem;
+ display: grid;
+
+ :first-child {
+ padding-top: 0.75rem;
+ }
+`;
+
+export const WindowContent = styled('div')`
+ height: calc(100% - 2.1875rem);
+ overflow-x: auto;
+`;
+
+export const WindowForm = styled('form')`
+ border-bottom: solid 0.0625rem rgb(31, 93, 117);
+ border-right: solid 0.0625rem rgb(31, 93, 117);
+ border-left: solid 0.0625rem rgb(31, 93, 117);
+ margin: 0 0.75rem 0.75rem 0.75rem;
+ width: calc(100% - 1.5rem);
+ text-align: left;
+`;
+
+export const WindowLabel = styled('label')`
+ text-transform: capitalize;
+ font-size: 0.8rem;
+`;
+
+export const WindowInput = styled('input')`
+ border: solid 0.0625rem rgb(31, 93, 117);
+ padding: 0.45rem 0.45rem 0.45rem 0.6rem;
+ background-color: rgb(5, 32, 58);
+ color: rgb(255, 255, 255);
+ border-radius: 5rem;
+ font-size: 0.8rem;
+`;
+
+export const WindowCheckbox = styled('div')`
+ border: solid 0.0625rem rgb(31, 93, 117);
+ background-color: rgb(5, 32, 58);
+ border-radius: 3.125rem;
+ position: relative;
+ width: 4.6875rem;
+ height: 1.5rem;
+ z-index: 0;
+
+ &:before {
+ color: rgb(255, 255, 255);
+ position: absolute;
+ font-weight: bold;
+ right: 0.625rem;
+ content: 'OFF';
+ top: 0.25rem;
+ }
+
+ &:after {
+ color: rgb(255, 255, 255);
+ position: absolute;
+ font-weight: bold;
+ left: 0.625rem;
+ content: 'ON';
+ top: 0.25rem;
+ z-index: 0;
+ }
+
+ label {
+ box-shadow: 0 0.125rem 0.5rem rgb(31, 93, 117);
+ background-color: rgb(255, 255, 255);
+ border-radius: 3.125rem;
+ position: absolute;
+ transition: 250ms;
+ cursor: pointer;
+ width: 1.875rem;
+ display: block;
+ top: 0.1875rem;
+ left: 0.25rem;
+ height: 1rem;
+ z-index: 1;
+ }
+
+ input[type="checkbox"] {
+ visibility: hidden;
+
+ &:checked + label {
+ left: 2.4375rem;
+ }
+ }
+`;
+
+export const WindowFormSubmit = styled('div')`
+ padding: 0.25rem 0.75rem 0.75rem 0.75rem;
+`;
+
+export const WindowFormButton = styled('button')`
+ border: solid 0.0625rem rgb(31, 93, 117);
+ background-color: rgb(5, 32, 58);
+ color: rgb(255, 255, 255);
+ padding: 0.5rem 0.25rem;
+ border-radius: 5rem;
+ width: 10rem;
+`;
+
+export const WindowFormClear = styled('button')`
+ border: solid 0.0625rem rgb(31, 93, 117);
+ padding: 0.45rem 0.4rem 0.55rem 0.4rem;
+ background-color: rgb(5, 32, 58);
+ color: rgb(255, 255, 255);
+ margin-left: 0.5rem;
+ border-radius: 5rem;
+ width: 2rem;
+
+ > svg {
+ vertical-align: bottom;
+ }
+`;
+
+export const WindowResult = styled('div')`
+ font-family: 'Courier New', Courier, monospace;
+ border: solid 0.0625rem rgb(31, 93, 117);
+ padding: 0.75rem 0.75rem 0 0.75rem;
+ margin: 0.75rem 0.75rem 0 0.75rem;
+ line-height: 1.2rem;
+ min-height: 7.5rem;
+ font-size: 0.8rem;
+ text-align: left;
+ white-space: pre;
+ overflow: auto;
+
+ ::-webkit-scrollbar {
+ height: 0.3rem;
+ width: 0.3rem;
+ }
+
+ > div {
+ border: solid 0.0625rem rgb(31, 113, 145);
+ background-color: rgb(5, 32, 58);
+ margin-bottom: 0.75rem;
+ padding: 0.25rem;
+ display: table;
+ width: 100%;
+ }
+
+ table {
+ border-collapse: collapse;
+ }
+
+ td,
+ th {
+ border: solid 0.0625rem rgb(31, 113, 145);
+ background-color: rgb(5, 32, 58);
+ padding: 0.25rem 0.5rem;
+ }
+
+ th {
+ background-color: rgb(31, 93, 117);
+ }
+`;
diff --git a/server/web/src/index.tsx b/server/web/src/index.tsx
new file mode 100644
index 0000000..810a349
--- /dev/null
+++ b/server/web/src/index.tsx
@@ -0,0 +1,56 @@
+import { transitions, positions, Provider as AlertProvider } from 'react-alert';
+import { GlobalStyle } from './design/GlobalStyle';
+import { AlertTemplate } from './components/Alert';
+import { Provider } from 'react-redux';
+import { store } from './redux/store';
+import ReactDOM from 'react-dom';
+import React from 'react';
+import App from './App';
+
+const alertOptions = {
+ // CONSTANT : alert parameters
+ position: positions.TOP_LEFT,
+ transition: transitions.FADE,
+ timeout: 3000
+};
+
+declare global {
+ interface Window {
+ showAlert: any;
+ eel: any;
+ }
+}
+
+window.eel.set_host(`ws://${window.location.host}`);
+window.oncontextmenu = () => false;
+
+window.addEventListener('load', () => {
+ window.addEventListener('error', (event: any) =>
+ console.log(`Window Error: ${event.message}`));
+
+ window.eel._websocket.addEventListener('open', () =>
+ console.log('Host Websocket Connected!'));
+
+ window.eel._websocket.addEventListener('error', (event: any) => {
+ console.log(`Websocket Error: ${event.message}`);
+ window.alert('Host Connection Error. Closing!');
+ window.close();
+ });
+
+ window.eel._websocket.addEventListener('close', () => {
+ window.alert('Host Disconnected. Closing!');
+ window.close();
+ });
+});
+
+ReactDOM.render(
+ <React.StrictMode>
+ <Provider store={store}>
+ <GlobalStyle />
+ <AlertProvider template={AlertTemplate} {...alertOptions}>
+ <App />
+ </AlertProvider>
+ </Provider>
+ </React.StrictMode>,
+ document.getElementById('root')
+);
diff --git a/server/web/src/interfaces/Activity.interface.ts b/server/web/src/interfaces/Activity.interface.ts
new file mode 100644
index 0000000..cb9cb77
--- /dev/null
+++ b/server/web/src/interfaces/Activity.interface.ts
@@ -0,0 +1,6 @@
+export interface IActivity {
+ resource_usage: string;
+ active_window: string;
+ idle_time: string;
+ unique_id: string;
+}
diff --git a/server/web/src/interfaces/Alert.interface.ts b/server/web/src/interfaces/Alert.interface.ts
new file mode 100644
index 0000000..0d58510
--- /dev/null
+++ b/server/web/src/interfaces/Alert.interface.ts
@@ -0,0 +1,4 @@
+export interface IAlert {
+ message: string;
+ type: string;
+}
diff --git a/server/web/src/interfaces/AllReducer.interface.ts b/server/web/src/interfaces/AllReducer.interface.ts
new file mode 100644
index 0000000..f70cc4a
--- /dev/null
+++ b/server/web/src/interfaces/AllReducer.interface.ts
@@ -0,0 +1,8 @@
+import { IClient } from './Client.interface';
+
+export interface IAllReducer {
+ clients: Map<string, IClient>;
+ session: Set<string>;
+ sessionLoad?: any;
+ clientsLoad?: any;
+}
diff --git a/server/web/src/interfaces/Client.interface.ts b/server/web/src/interfaces/Client.interface.ts
new file mode 100644
index 0000000..cb7f036
--- /dev/null
+++ b/server/web/src/interfaces/Client.interface.ts
@@ -0,0 +1,11 @@
+export interface IClient {
+ // CONSTANT : does not including the other
+ // client categories not explicitly accessed
+ resource_usage: string;
+ active_window: string;
+ country_code: string;
+ connect_ip: string;
+ idle_time: string;
+ unique_id: string;
+ country: string;
+}
diff --git a/server/web/src/interfaces/Stream.interface.ts b/server/web/src/interfaces/Stream.interface.ts
new file mode 100644
index 0000000..45b5139
--- /dev/null
+++ b/server/web/src/interfaces/Stream.interface.ts
@@ -0,0 +1,4 @@
+export interface IStream {
+ source: string;
+ title: string;
+}
diff --git a/server/web/src/interfaces/components/Card.interface.ts b/server/web/src/interfaces/components/Card.interface.ts
new file mode 100644
index 0000000..911f1ee
--- /dev/null
+++ b/server/web/src/interfaces/components/Card.interface.ts
@@ -0,0 +1,7 @@
+export interface IProps {
+ removeStream: any;
+ source: string;
+ title: string;
+}
+
+export interface IState {}
diff --git a/server/web/src/interfaces/components/Footer.interface.ts b/server/web/src/interfaces/components/Footer.interface.ts
new file mode 100644
index 0000000..20d9956
--- /dev/null
+++ b/server/web/src/interfaces/components/Footer.interface.ts
@@ -0,0 +1,8 @@
+export interface IProps {}
+
+export interface IState {
+ showHelp: boolean;
+ address: string;
+ windows: any;
+ help: object;
+}
diff --git a/server/web/src/interfaces/components/Select.interface.ts b/server/web/src/interfaces/components/Select.interface.ts
new file mode 100644
index 0000000..994a5a4
--- /dev/null
+++ b/server/web/src/interfaces/components/Select.interface.ts
@@ -0,0 +1,12 @@
+export interface ISelect {
+ blacklistRemove?: any;
+ sessionRemove?: any;
+ blacklistAdd?: any;
+ clientRemove?: any;
+ sessionAdd?: any;
+ left?: number;
+ show: boolean;
+ top?: number;
+}
+
+export interface IState {}
diff --git a/server/web/src/interfaces/components/Server.interface.ts b/server/web/src/interfaces/components/Server.interface.ts
new file mode 100644
index 0000000..67502c9
--- /dev/null
+++ b/server/web/src/interfaces/components/Server.interface.ts
@@ -0,0 +1,15 @@
+import { IClient } from '../Client.interface';
+import { ISelect } from './Select.interface';
+
+export interface IProps {
+ clients: Map<string, IClient>;
+ session: Set<string>;
+ clientsLoad?: any;
+ sessionLoad?: any;
+}
+
+export interface IState {
+ clients: Map<string, IClient>;
+ session: Set<string>;
+ selectData: ISelect;
+}
diff --git a/server/web/src/interfaces/components/Sidebar.interface.ts b/server/web/src/interfaces/components/Sidebar.interface.ts
new file mode 100644
index 0000000..d270713
--- /dev/null
+++ b/server/web/src/interfaces/components/Sidebar.interface.ts
@@ -0,0 +1,8 @@
+import { IStream } from '../Stream.interface';
+
+export interface IProps {}
+
+export interface IState {
+ showContent: boolean;
+ streams: IStream[];
+}
diff --git a/server/web/src/interfaces/components/Window.interface.ts b/server/web/src/interfaces/components/Window.interface.ts
new file mode 100644
index 0000000..7bfe7db
--- /dev/null
+++ b/server/web/src/interfaces/components/Window.interface.ts
@@ -0,0 +1,22 @@
+interface IPos {
+ x: number;
+ y: number;
+}
+
+export interface IProps {
+ requestType: string;
+ requestArgs: any;
+ hightlight: any;
+ data?: string[];
+ destroy: any;
+ toggle: any;
+ pos: IPos;
+}
+
+export interface IState {
+ fullscreen: boolean;
+ dragging: boolean;
+ result: string[];
+ pos: IPos;
+ rel: any;
+}
diff --git a/server/web/src/interfaces/design/AlertDesign.interface.ts b/server/web/src/interfaces/design/AlertDesign.interface.ts
new file mode 100644
index 0000000..9ee6267
--- /dev/null
+++ b/server/web/src/interfaces/design/AlertDesign.interface.ts
@@ -0,0 +1,3 @@
+export interface IAlertButton {
+ bgColor: string;
+}
diff --git a/server/web/src/interfaces/design/FooterDesign.interface.ts b/server/web/src/interfaces/design/FooterDesign.interface.ts
new file mode 100644
index 0000000..84b4d00
--- /dev/null
+++ b/server/web/src/interfaces/design/FooterDesign.interface.ts
@@ -0,0 +1,3 @@
+export interface IFooterDropdownContent {
+ active: boolean;
+}
diff --git a/server/web/src/interfaces/design/ServerDesign.interface.ts b/server/web/src/interfaces/design/ServerDesign.interface.ts
new file mode 100644
index 0000000..07c750b
--- /dev/null
+++ b/server/web/src/interfaces/design/ServerDesign.interface.ts
@@ -0,0 +1,7 @@
+export interface IServerTableRow {
+ activeSession?: string;
+}
+
+export interface IServerTableBar {
+ width?: string;
+}
diff --git a/server/web/src/interfaces/design/SidebarDesign.interface.ts b/server/web/src/interfaces/design/SidebarDesign.interface.ts
new file mode 100644
index 0000000..eb0fe49
--- /dev/null
+++ b/server/web/src/interfaces/design/SidebarDesign.interface.ts
@@ -0,0 +1,3 @@
+export interface ISidebarSlide {
+ active: boolean;
+}
diff --git a/server/web/src/react-app-env.d.ts b/server/web/src/react-app-env.d.ts
new file mode 100644
index 0000000..6431bc5
--- /dev/null
+++ b/server/web/src/react-app-env.d.ts
@@ -0,0 +1 @@
+/// <reference types="react-scripts" />
diff --git a/server/web/src/redux/actions.ts b/server/web/src/redux/actions.ts
new file mode 100644
index 0000000..887b808
--- /dev/null
+++ b/server/web/src/redux/actions.ts
@@ -0,0 +1,55 @@
+import { IActivity } from '../interfaces/Activity.interface';
+import { IClient } from '../interfaces/Client.interface';
+
+export type ClientsLoadType = { type: 'CLIENTS_LOAD'; payload: Map<string, IClient>; };
+export type SessionLoadType = { type: 'SESSION_LOAD'; payload: Set<string>; };
+export type SessionAllType = { type: 'SESSION_ALL'; };
+export type SessionCloseType = { type: 'SESSION_CLOSE'; };
+export type SessionAddType = { type: 'SESSION_ADD'; payload: string; };
+export type SessionRemoveType = { type: 'SESSION_REMOVE'; payload: string; };
+export type ClientAddType = { type: 'CLIENT_ADD'; payload: { unique_id: string, client: IClient; }; };
+export type ClientRemoveType = { type: 'CLIENT_REMOVE'; payload: string; };
+export type ActivityUpdateType = { type: 'ACTIVITY_UPDATE'; payload: IActivity; };
+
+export const clientsLoad = (clients: Map<string, IClient>): ClientsLoadType => ({
+ type: 'CLIENTS_LOAD',
+ payload: clients
+});
+
+export const sessionLoad = (session: Set<string>): SessionLoadType => ({
+ type: 'SESSION_LOAD',
+ payload: session
+});
+
+export const sessionAll = (): SessionAllType => ({
+ type: 'SESSION_ALL'
+});
+
+export const sessionClose = (): SessionCloseType => ({
+ type: 'SESSION_CLOSE'
+});
+
+export const sessionAdd = (unique_id: string): SessionAddType => ({
+ type: 'SESSION_ADD',
+ payload: unique_id
+});
+
+export const sessionRemove = (unique_id: string): SessionRemoveType => ({
+ type: 'SESSION_REMOVE',
+ payload: unique_id
+});
+
+export const clientAdd = (unique_id: string, client: IClient): ClientAddType => ({
+ type: 'CLIENT_ADD',
+ payload: { unique_id, client }
+});
+
+export const clientRemove = (unique_id: string): ClientRemoveType => ({
+ type: 'CLIENT_REMOVE',
+ payload: unique_id
+});
+
+export const activityUpdate = (activity: IActivity): ActivityUpdateType => ({
+ type: 'ACTIVITY_UPDATE',
+ payload: activity
+});
diff --git a/server/web/src/redux/allReducer.ts b/server/web/src/redux/allReducer.ts
new file mode 100644
index 0000000..fe72005
--- /dev/null
+++ b/server/web/src/redux/allReducer.ts
@@ -0,0 +1,83 @@
+import { IAllReducer } from '../interfaces/AllReducer.interface';
+import { IClient } from '../interfaces/Client.interface';
+import {
+ ClientsLoadType,
+ SessionLoadType,
+ SessionAllType,
+ SessionCloseType,
+ SessionAddType,
+ SessionRemoveType,
+ ClientAddType,
+ ClientRemoveType,
+ ActivityUpdateType
+} from './actions';
+
+const initialState: IAllReducer = {
+ clients: new Map<string, IClient>(),
+ session: new Set<string>()
+};
+
+export const allReducer = (
+ state: IAllReducer = initialState,
+ action:
+ | ClientsLoadType
+ | SessionLoadType
+ | SessionAllType
+ | SessionCloseType
+ | SessionAddType
+ | SessionRemoveType
+ | ClientAddType
+ | ClientRemoveType
+ | ActivityUpdateType
+) => {
+ switch (action.type) {
+ case 'CLIENTS_LOAD': {
+ return { ...state, clients: new Map<string, IClient>(action.payload) };
+ }
+ case 'SESSION_LOAD': {
+ return { ...state, session: new Set<string>(action.payload) };
+ }
+ case 'SESSION_ALL': {
+ return { ...state, session: new Set<string>(state.clients.keys()) };
+ }
+ case 'SESSION_CLOSE': {
+ return { ...state, session: new Set<string>() };
+ }
+ case 'SESSION_ADD': {
+ const updatedSession = new Set<string>(state.session);
+ updatedSession.add(action.payload);
+ return { ...state, session: updatedSession };
+ }
+ case 'SESSION_REMOVE': {
+ const updatedSession = new Set<string>(state.session);
+ updatedSession.delete(action.payload);
+ return { ...state, session: updatedSession };
+ }
+ case 'CLIENT_ADD': {
+ const updatedClients = new Map<string, IClient>(state.clients);
+ updatedClients.set(action.payload.unique_id, action.payload.client);
+ return { ...state, clients: updatedClients };
+ }
+ case 'CLIENT_REMOVE': {
+ const updatedClients = new Map<string, IClient>(state.clients);
+ updatedClients.delete(action.payload);
+ return { ...state, clients: updatedClients };
+ }
+ case 'ACTIVITY_UPDATE': {
+ const { unique_id, active_window, idle_time, resource_usage } = action.payload;
+ const updatedClients = new Map<string, IClient>(state.clients);
+ const client = updatedClients.get(unique_id);
+
+ if (client !== undefined) {
+ client.active_window = active_window;
+ client.idle_time = idle_time;
+ client.resource_usage = resource_usage;
+ updatedClients.set(unique_id, client);
+ }
+
+ return { ...state, clients: updatedClients };
+ }
+ default:
+ return state;
+ }
+};
diff --git a/server/web/src/redux/store.ts b/server/web/src/redux/store.ts
new file mode 100644
index 0000000..8321e8e
--- /dev/null
+++ b/server/web/src/redux/store.ts
@@ -0,0 +1,4 @@
+import { createStore } from 'redux';
+import { allReducer } from './allReducer';
+
+export const store = createStore(allReducer);