diff options
author | AL-LCL <alvin@alvinhavel.com> | 2023-05-19 10:39:49 +0200 |
---|---|---|
committer | AL-LCL <alvin@alvinhavel.com> | 2023-05-19 10:39:49 +0200 |
commit | 58ebd3bc0f00c532e97e9a5571471ffab87934ba (patch) | |
tree | 6e099e59af07206df6edf2b0c585d0c5a466d4bd /server/web/src/components |
Diffstat (limited to 'server/web/src/components')
-rw-r--r-- | server/web/src/components/Alert.tsx | 14 | ||||
-rw-r--r-- | server/web/src/components/Card.tsx | 80 | ||||
-rw-r--r-- | server/web/src/components/Footer.tsx | 347 | ||||
-rw-r--r-- | server/web/src/components/Select.tsx | 56 | ||||
-rw-r--r-- | server/web/src/components/Server.tsx | 571 | ||||
-rw-r--r-- | server/web/src/components/Sidebar.tsx | 136 | ||||
-rw-r--r-- | server/web/src/components/Window.tsx | 308 |
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; |