summaryrefslogtreecommitdiff
path: root/server/web/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'server/web/src/components')
-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
7 files changed, 1512 insertions, 0 deletions
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;