/* Copyright © XlXi 2022 */ import { Component, createRef } from 'react'; import classNames from 'classnames/bind'; import axios from 'axios'; import { buildGenericApiUrl } from '../util/HTTP.js'; import Loader from './Loader'; axios.defaults.withCredentials = true; class DeploymentUploadModal extends Component { constructor(props) { super(props); this.state = { deployments: [] }; this.updateInterval = null; this.ModalRef = createRef(); this.Modal = null; } componentDidMount() { this.Modal = new Bootstrap.Modal( this.ModalRef.current, { backdrop: 'static', keyboard: false } ); this.Modal.show(); this.ModalRef.current.addEventListener('hidden.bs.modal', (event) => { this.props.setModal(null); }) this.setState({ deployments: this.props.deployments }); this.updateInterval = setInterval(function() { let deployments = this.state.deployments; deployments.map((component, index) => { axios.get(buildGenericApiUrl('api', `admin/v1/deploy?version=${ component.version }`)) .then(res => { deployments[index] = { ...component, ...res.data }; // XlXi: -_- //deployment['status'] = res.data['status']; //deployment['message'] = res.data['message']; //deployment['progress'] = res.data['progress']; }); }); this.setState({ deployments: deployments }); }.bind(this), 2000); } componentWillUnmount() { this.Modal.dispose(); clearInterval(this.updateInterval); } render() { return (
Deployment Status
{ this.state.deployments.map((deployment, index) => ( <>
Deploying { capitalizeFirstLetter(deployment.key) } { deployment.version }

{ deployment.message }

{ index != this.state.deployments.length-1 ?
: null } )) }
); } } class DeploymentCard extends Component { constructor(props) { super(props); } render() { return (
{ this.props.name }
{ this.props.children }
); } } class RevertDeploymentCard extends Component { constructor(props) { super(props); this.state = { loading: true, deployments: [] }; } componentDidMount() { let deployType = 'Unknown'; switch(this.props.index) { case 'client': deployType = 'WindowsPlayer' break; case 'studio': deployType = 'Studio' break; } let deployHistoryRegex = new RegExp(`New ${deployType} version-[a-zA-Z0-9]{16} at \\d{1,2}\\/\\d{1,2}\\/\\d{4} \\d{1,2}:\\d{1,2}:\\d{1,2} (AM|PM), file version: (\\d+(, )?){4}\\.{3}Done!`, 'g'); axios.get(buildGenericApiUrl('setup', 'DeployHistory.txt')) .then(res => { let deployments = res.data.split(/\r?\n/).reverse(); deployments = deployments.filter((deployment) => deployHistoryRegex.test(deployment)).slice(0, 30); let realDeployments = []; deployments.map((deployment) => { let newDeployment = { type: deployType, hash: deployment.match(/version-[a-zA-Z0-9]{16}/)[0], version: deployment.match(/file version: (\d+(, )?){4}/)[0].replace('file version: ', ''), date: deployment.match(/at \d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{1,2}:\d{1,2} (AM|PM)/)[0].replace('at ', '') }; realDeployments.push(newDeployment); }); this.setState({ loading: false, deployments: realDeployments }); }); } render() { return (
Revert Deployment

Select a previous deployment below to roll back the { this.props.index } version.

); } } class PushDeploymentCard extends Component { constructor(props) { super(props); this.state = { drag: false, showRequiredFiles: false, files: [], options: [] }; this.dragDropBox = createRef(); this.dragCounter = 0; this.neededFiles = [ 'content-fonts.zip', 'content-music.zip', 'content-particles.zip', 'content-sky.zip', 'content-sounds.zip', 'content-terrain.zip', 'content-textures.zip', 'content-textures2.zip', 'content-textures3.zip', 'shaders.zip', 'redist.zip', 'Libraries.zip' ]; this.handleDrag = this.handleDrag.bind(this); this.handleDragIn = this.handleDragIn.bind(this); this.handleDragOut = this.handleDragOut.bind(this); this.handleDrop = this.handleDrop.bind(this); this.handleDropUi = this.handleDropUi.bind(this); this.fileExists = this.fileExists.bind(this); this.removeFile = this.removeFile.bind(this); this.setOptions = this.setOptions.bind(this); } componentDidMount() { switch(this.props.index) { case 'client': this.neededFiles = this.neededFiles.concat([ 'PlayerPdb.zip', 'VirtuBrick.zip', 'VirtuBrickPlayerLauncher.exe' ]); break; case 'studio': this.neededFiles = this.neededFiles.concat([ 'BuiltInPlugins.zip', 'imageformats.zip', 'content-scripts.zip', 'StudioPdb.zip', 'VirtuBrickStudio.zip', 'VirtuBrickStudioLauncherBeta.exe' ]); break; } this.setState({ showRequiredFiles: true }); let ddb = this.dragDropBox.current ddb.addEventListener('dragenter', this.handleDragIn) ddb.addEventListener('dragleave', this.handleDragOut) ddb.addEventListener('dragover', this.handleDrag) ddb.addEventListener('drop', this.handleDrop) } componentWillUnmount() { let ddb = this.dragDropBox.current ddb.removeEventListener('dragenter', this.handleDragIn) ddb.removeEventListener('dragleave', this.handleDragOut) ddb.removeEventListener('dragover', this.handleDrag) ddb.removeEventListener('drop', this.handleDrop) } handleDrag(evt) { evt.preventDefault(); evt.stopPropagation(); } handleDragIn(evt) { evt.preventDefault(); evt.stopPropagation(); this.dragCounter++; if (evt.dataTransfer.items && evt.dataTransfer.items.length > 0) { this.setState({drag: true}); } } handleDragOut(evt) { evt.preventDefault(); evt.stopPropagation(); this.dragCounter--; if (this.dragCounter === 0) { this.setState({drag: false}); } } handleDrop(evt) { evt.preventDefault(); evt.stopPropagation(); this.setState({drag: false}); if (evt.dataTransfer.files && evt.dataTransfer.files.length > 0) { this.handleDropUi(evt.dataTransfer.files); evt.dataTransfer.clearData(); this.dragCounter = 0; } } handleDropUi(files) { let fileList = this.state.files; for (var i = 0; i < files.length; i++) { let file = files[i]; if (!file) continue; if(this.fileExists(file.name)) continue; fileList.push(file); } this.setState({files: fileList}); this.props.updateDeploymentFiles(fileList); } fileExists(fileName) { return this.state.files.some(file => file.name.toLowerCase() === fileName.toLowerCase()); } removeFile(fileName) { this.setState(prevState => ({ files: prevState.files.filter((file) => file.name.toLowerCase() !== fileName.toLowerCase()) })); this.props.updateDeploymentFiles(this.state.files); } setOptions(key, value) { let options = this.state.options; if(value != '') options[key] = value; else delete options[key]; this.setState({ options: options }); this.props.updateDeploymentOptions(options) } render() { return (
Deployment Files

Drag-and-Drop the necessary files into the box below. Any unneeded files will be discarded when uploading.

{/* XlXi: Reusing game cards here because they were already exactly what I wanted. */} { this.state.files.length == 0 ?

Drop files here.

: this.state.files.map((file, index) => { let fileType = /(?:\.([^.]+))?$/.exec(file.name)[1].toLowerCase(); let fileIconClasses = { 'm-auto': true, 'fs-1': true, 'fa-regular': true }; switch(fileType) { case 'exe': fileIconClasses['fa-browser'] = true; break; case 'zip': fileIconClasses['fa-file-zipper'] = true; break; default: fileIconClasses['fa-file'] = true; break; } return (

{ file.name }

); }) }
{ this.state.showRequiredFiles ? <>
Needed Files
{ this.neededFiles.map((fileName) => { let fileExists = this.fileExists(fileName); return (

{ fileName }

); }) }
: null }
{ this.props.index == 'client' ? <>
Optional Configuration

Only change if you've updated the security settings on the client/rcc. Shutting down game servers will delay deployment by 10 minutes.

this.setOptions('rccAccessKey', changeEvent.target.value) } /> this.setOptions('versionCompatiblityFuzzyKey', changeEvent.target.value) } /> : null }
); } } // https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript function capitalizeFirstLetter(string) { return string.charAt(0).toUpperCase() + string.slice(1); } class Deployer extends Component { constructor(props) { super(props); this.state = { showModal: false, deployType: 'deploy', showTypeSwitchError: false, deploying: false, deployments: [] }; this.visibleModal = null; this.updateDeploymentFiles = this.updateDeploymentFiles.bind(this); this.deploymentExists = this.deploymentExists.bind(this); this.getDeployment = this.getDeployment.bind(this); this.createDeployment = this.createDeployment.bind(this); this.removeDeployment = this.removeDeployment.bind(this); this.trySetDeployType = this.trySetDeployType.bind(this); this.uploadDeploymentFiles = this.uploadDeploymentFiles.bind(this); this.uploadDeployments = this.uploadDeployments.bind(this); this.setModal = this.setModal.bind(this); } updateDeploymentFiles(key, files) { if(this.state.deploying) // XlXi: Prevent messing with the component when we're deploying. return; let deployment = this.getDeployment(key); deployment['files'] = files; } updateDeploymentOptions(key, options) { if(this.state.deploying) // XlXi: Prevent messing with the component when we're deploying. return; let deployment = this.getDeployment(key); deployment['extraOptions'] = options; } deploymentExists(key) { return this.state.deployments.some(deployment => deployment.key === key); } getDeployment(key) { return this.state.deployments.find((deployment) => deployment.key === key); } createDeployment(key) { if(this.state.deploying) // XlXi: Prevent messing with the component when we're deploying. return; let newElement = { key: key, name: capitalizeFirstLetter(this.state.deployType) + ' ' + capitalizeFirstLetter(key), files: [], extraOptions: [] }; this.setState(prevState => ({ deployments: [...prevState.deployments, newElement] })); } removeDeployment(key) { if(this.state.deploying) // XlXi: Prevent messing with the component when we're deploying. return; this.setState(prevState => ({ deployments: prevState.deployments.filter((deployment) => deployment.key !== key) })); } trySetDeployType(evt, type) { if(this.state.deploying) // XlXi: Prevent messing with the component when we're deploying. return; if(!evt.target.checked) return; if(this.state.deployType != type && this.state.deployments.length != 0) { this.setState({ showTypeSwitchError: true }); return; } this.setState({ deployType: type, showTypeSwitchError: false }); } uploadDeploymentFiles(key, version) { let deployment = this.getDeployment(key); let bodyFormData = new FormData(); deployment.files.map((file) => { bodyFormData.append('file[]', file); }); Object.keys(deployment.extraOptions).forEach(function(key, index) { bodyFormData.append(key, deployment.extraOptions[key]); }); axios.post( buildGenericApiUrl('api', `admin/v1/deploy/${ deployment.version }`), bodyFormData ); } uploadDeployments() { if(this.state.deploying) // XlXi: Prevent multiple deployments of the same files. return; this.setState({ deploying: true }); let requests = 0; this.state.deployments.map((component, index) => { axios.get(buildGenericApiUrl('api', `admin/v1/deploy?type=deploy&app=${ component.key }`)) .then(res => { requests++; this.state.deployments[index] = {...component, ...res.data}; if(requests == this.state.deployments.length) this.setModal(); this.uploadDeploymentFiles(component.key, component.version); }); }); //this.setState({ deployments: [] }); } setModal(modal = null) { this.visibleModal = modal; if(modal) { this.setState({'showModal': true}); } else { this.setState({'showModal': false}); } } render() { return ( <>
Deployment Options
this.trySetDeployType(e, 'deploy') } checked={ this.state.deployType == 'deploy' } /> this.trySetDeployType(e, 'revert') } checked={ this.state.deployType == 'revert' } />


{ this.state.showTypeSwitchError ?
Remove your { this.state.deployType == 'deploy' ? 'deployments' : 'reversions' } to change the uploader type.
: null }
{ this.state.deployType == 'deploy' ? 'Staged Deployments' : 'Revert Deployments' }
{ this.state.deployments.length == 0 ?

No deployments are selected.

: this.state.deployments.map((component) => { // XlXi: surely theres a better way to do this..... switch(this.state.deployType) { case 'deploy': return this.updateDeploymentFiles(component.key, files) } updateDeploymentOptions={ (options)=>this.updateDeploymentOptions(component.key, options) } /> case 'revert': return } }) }
{ this.state.showModal ? this.visibleModal : null } ); } } export default Deployer;