/*
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.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;