diff options
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); | 
