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 |
Diffstat (limited to 'server/web/src')
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); |