899 lines
47 KiB
Plaintext
899 lines
47 KiB
Plaintext
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<base href="<%= basePath %>">
|
|
<meta charset="utf-8">
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
|
<meta name="description" content="txAdmin - remotely Manage & Monitor your GTA5 FiveM Server">
|
|
<meta name="author" content="André Tabarra">
|
|
<title><%= headerTitle %></title>
|
|
<link href="css/simple-line-icons.css?txVer=<%= txAdminVersion %>" rel="stylesheet">
|
|
<link href="css/coreui.min.css?txVer=<%= txAdminVersion %>" rel="stylesheet">
|
|
<link href="css/dark.css?txVer=<%= txAdminVersion %>" rel="stylesheet">
|
|
<link rel="shortcut icon" type="image/png" href="img/favicon_default.png" id="favicon" />
|
|
<link rel="stylesheet" href="css/txAdmin.css?txVer=<%= txAdminVersion %>">
|
|
|
|
<!-- Page CSS -->
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
|
<link rel="stylesheet" href="https://unpkg.com/materialize-stepper@3.1.0/dist/css/mstepper.min.css">
|
|
|
|
<style>
|
|
code:not(.keep-original){
|
|
color: #321fdb!important;
|
|
}
|
|
body.theme--dark code:not(.keep-original) {
|
|
color: #0d86ff !important;
|
|
}
|
|
.bigbutton{
|
|
cursor: pointer;
|
|
background-color: #f7f7f7;
|
|
-webkit-transition: background-color 250ms linear, color 50ms linear;
|
|
-ms-transition: background-color 250ms linear, color 50ms linear;
|
|
transition: background-color 250ms linear, color 50ms linear;
|
|
}
|
|
.bigbutton:hover:not(.disabled){
|
|
background-color: #2196f3 !important;
|
|
color: white;
|
|
}
|
|
.bigbutton.disabled:hover{
|
|
background-color: #424242 !important;
|
|
color: white;
|
|
}
|
|
.bigbutton.disabled{
|
|
cursor: not-allowed;
|
|
}
|
|
.bigbutton em{
|
|
-webkit-transition: color 50ms linear;
|
|
-ms-transition: color 50ms linear;
|
|
transition: color 50ms linear;
|
|
}
|
|
.bigbutton:hover:not(.disabled) em{
|
|
color: var(--light)!important;
|
|
}
|
|
.stepper{
|
|
list-style: none;
|
|
padding-inline-start: 20px;
|
|
}
|
|
.deployment-type-card{
|
|
border-radius: 15px;
|
|
margin: 0.75em;
|
|
padding: 0.75rem 1rem;
|
|
}
|
|
.deployment-type-card > .card-body{
|
|
padding: 0.75rem !important;
|
|
}
|
|
.template-cards{
|
|
margin: 5px;
|
|
padding: 8px;
|
|
height: 5.1rem;
|
|
border-radius: 5px;
|
|
}
|
|
.template-cards > span{
|
|
overflow: hidden;
|
|
word-wrap: break-word;
|
|
text-overflow: ellipsis;
|
|
display: -webkit-box;
|
|
-webkit-box-orient: vertical;
|
|
}
|
|
.template-cards > .titleText{
|
|
-webkit-line-clamp: 1;
|
|
}
|
|
.template-cards > .description{
|
|
-webkit-line-clamp: 2;
|
|
}
|
|
.template-cards > .titleText::before{
|
|
content: "⭐";
|
|
}
|
|
.template-cards > .title{
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
.template-cards .tagList{
|
|
display: flex;
|
|
flex-direction: row-reverse;
|
|
}
|
|
|
|
.recipeTag {
|
|
display: inline-block;
|
|
padding: .25em .4em;
|
|
font-size: 75%;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
vertical-align: baseline;
|
|
border-radius: .25rem;
|
|
margin: 0px 0.15rem;
|
|
}
|
|
.recipeTagFivem {
|
|
color: #FF8637;
|
|
border: 1px solid #FF8637;
|
|
}
|
|
.recipeTagRedm {
|
|
color: #FA0211;
|
|
border: 1px solid #FA0211;
|
|
}
|
|
.recipeTagDefault {
|
|
color: #6b6b6b;
|
|
border: 1px solid #6b6b6b;
|
|
}
|
|
body.theme--dark .recipeTagDefault {
|
|
color: #d6d6d6;
|
|
border: 1px solid #d6d6d6;
|
|
}
|
|
</style>
|
|
|
|
<!-- injected consts -->
|
|
<script><%- jsInjection %></script>
|
|
</head>
|
|
|
|
<body class="c-app flex-row <%= uiTheme %>">
|
|
<div class="container">
|
|
|
|
<!-- Stepper Row -->
|
|
<div class="row justify-content-center">
|
|
<div class="col-12 col-md-12 col-lg-8">
|
|
<div class="card fade-in">
|
|
<div class="card-body">
|
|
<ul class="stepper linear">
|
|
|
|
<!-- Server Name -->
|
|
<li class="step <%= skipServerName ? 'done' : 'active' %>">
|
|
<div class="step-title waves-effect">Server Name</div>
|
|
<div class="step-content">
|
|
A <strong>short</strong> server name to be used in txAdmin interface and
|
|
Chat/Discord messages.
|
|
<input type="text" class="form-control mt-2" id="frmName" maxlength="22"
|
|
minlength="3" placeholder="Happy Server" value="<%= serverName %>" required>
|
|
<span id="frmNameError" class="text-danger fade-in d-none"></span>
|
|
<div class="step-actions">
|
|
<button class="btn btn-pill btn-secondary previous-step"
|
|
type="button">Back</button>
|
|
<button class="btn btn-pill btn-info next-step" type="button"
|
|
data-feedback="validateName" id="frmNameSubmitBtn">Next</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
|
|
|
|
<!-- Deployment Type -->
|
|
<li class="step <%= skipServerName ? 'active' : '' %>">
|
|
<div class="step-title waves-effect">Deployment Type</div>
|
|
<div class="step-content">
|
|
<strong>Select</strong> how you want to setup your server:
|
|
<div class="bigbutton deployment-type-card border-dark next-step" onclick="selectDeploymentType('popular')">
|
|
<span class="title"><strong>⭐ Popular Recipes</strong> <span class="badge badge-danger">RECOMMENDED</span></span>
|
|
<br>
|
|
<span class="description">
|
|
Select a template from a curated list of community favorites. <br>
|
|
This includes QBCore, ESX and a Default template for you to customize.
|
|
</span>
|
|
</div>
|
|
<div class="bigbutton deployment-type-card border-dark next-step" onclick="selectDeploymentType('local')">
|
|
<span class="title"><strong>📁 Existing Server Data</strong></span>
|
|
<br>
|
|
<span class="description">
|
|
Select an existing server data folder in the host. <br>
|
|
Only select this option if you already have a <code class="keep-original">server.cfg</code> and a <code class="keep-original">resources</code> folder. <br>
|
|
If you are not running fxserver in your computer, you need to first upload those files to the server's host.
|
|
</span>
|
|
</div>
|
|
<div class="bigbutton deployment-type-card border-dark next-step" onclick="selectDeploymentType('remote')">
|
|
<span class="title"><strong>📥 Remote URL Template</strong></span>
|
|
<br>
|
|
<span class="description">
|
|
Based on a Recipe URL in the YAML format. <br>
|
|
You will have the option to edit the Recipe before running it.
|
|
</span>
|
|
</div>
|
|
<div class="bigbutton deployment-type-card border-dark next-step" onclick="selectDeploymentType('custom')">
|
|
<span class="title"><strong>📑 Custom Template</strong></span>
|
|
<br>
|
|
<span class="description">
|
|
This is recommended if you have a custom recipe-based server or if you are writing your own recipe.
|
|
You will be asked for the recipe right after this page.
|
|
</span>
|
|
</div>
|
|
<div class="step-actions">
|
|
<button class="btn btn-pill btn-secondary previous-step"
|
|
type="button">Back</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
|
|
|
|
<!-- Step 4 -->
|
|
<li class="step">
|
|
<div class="step-title waves-effect" id="s4Title"><i>please select deployment type</i></div>
|
|
<div class="step-content">
|
|
<!-- select template buttons -->
|
|
<template id="s4PopularTemplateCardTemplate">
|
|
<div class="bigbutton template-cards border-dark next-step">
|
|
<div class="title">
|
|
<span class="titleText">
|
|
<strong></strong>
|
|
</span>
|
|
<div class="tagList"></div>
|
|
</div>
|
|
<span class="description"></span>
|
|
</div>
|
|
</template>
|
|
<div class="d-none" id="s4PopularTemplateDiv">
|
|
<div class="row" id="s4PopularTemplateRow"></div>
|
|
|
|
<div class="step-actions">
|
|
<button class="btn btn-pill btn-secondary previous-step"
|
|
type="button">Back</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- input url with some minimal backend validation -->
|
|
<div class="d-none" id="s4RemoteTemplateDiv">
|
|
The URL for the remote recipe, or the forum thread. <br>
|
|
You can discover new recipes by visiting the <a href="https://forum.cfx.re/c/development/releases/7" target="_blank" rel="noopener noreferrer">CFX Forum</a>.
|
|
<input type="text" class="form-control mt-2" id="s4RemoteTemplateInput"
|
|
placeholder="https://raw.githubusercontent.com/tabarra/CFX-Default-recipe/main/recipe.yaml">
|
|
<span id="s4RemoteTemplateInputError" class="text-danger fade-in d-none"></span>
|
|
<div class="step-actions">
|
|
<button class="btn btn-pill btn-secondary previous-step"
|
|
type="button">Back</button>
|
|
<button class="btn btn-pill btn-info next-step" type="button"
|
|
data-feedback="validateRemoteTemplate">Next</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- explain and show some doc links -->
|
|
<div class="d-none" id="s4CustomTemplateDiv">
|
|
<p>
|
|
In the next page you will be prompted to insert your custom Recipe. <br>
|
|
If you are developing your own recipes, we highly recommend you to check the following:
|
|
<ul class="font-weight-bold">
|
|
<li><a href="https://github.com/tabarra/txAdmin/blob/master/docs/recipe.md" target="_blank" rel="noopener">Recipe Documentation</a></li>
|
|
<li><a href="https://github.com/tabarra/txAdmin-recipes" target="_blank" rel="noopener">Example Recipes</a></li>
|
|
<li><a href="https://discord.gg/NsXGTszYjK" target="_blank" rel="noopener">Discord Support</a></li>
|
|
</ul>
|
|
</p>
|
|
|
|
<div class="step-actions">
|
|
<button class="btn btn-pill btn-secondary previous-step"
|
|
type="button">Back</button>
|
|
<button class="btn btn-pill btn-info next-step"
|
|
type="button">Next</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- input local data folder -->
|
|
<div class="d-none" id="s4LocalDataPathDiv">
|
|
The folder that <strong>contains</strong> the <code>resources</code> and
|
|
<code>cache</code> folders, usually it's here that you put your <code>server.cfg</code>. <br>
|
|
Also known as Base Folder. <br>
|
|
<% if (hasCustomDataPath) { %>
|
|
<span class="font-weight-bold"><%= hostConfigSource %>: this path should start with <code><%= dataPath %></code>.</span>
|
|
<% } %>
|
|
<input type="text" class="form-control mt-2" id="s4LocalDataPathInput"
|
|
placeholder="C:/Users/Admin/Desktop/server01">
|
|
<span id="s4LocalDataPathInputError" class="text-danger fade-in d-none"></span>
|
|
<div class="step-actions">
|
|
<button class="btn btn-pill btn-secondary previous-step"
|
|
type="button">Back</button>
|
|
<button class="btn btn-pill btn-info next-step" type="button"
|
|
data-feedback="validateLocalDataFolder" id="s4LocalDataPathInputSubmitBtn">Next</button>
|
|
<button class="btn btn-pill btn-outline-primary d-none fade-in" type="button"
|
|
id="s4LocalDataPathInputFixBtn"
|
|
data-target="s4LocalDataPathInput">Accept Fix</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</li>
|
|
|
|
|
|
<!-- Step 5 -->
|
|
<li class="step">
|
|
<div class="step-title waves-effect" id="s5Title"><i>please select deployment type</i></div>
|
|
<div class="step-content" id="s5Content">
|
|
|
|
<!-- input local data folder -->
|
|
<div class="d-none" id="s5DeployerTargetDiv">
|
|
The folder where the server will be deployed to. <br>
|
|
This folder will contain all your resources and configuration files. <br>
|
|
<% if (hasCustomDataPath) { %>
|
|
<span class="font-weight-bold"><%= hostConfigSource %>: this path must start with <code><%= dataPath %></code>.</span> <br>
|
|
<% } %>
|
|
<strong>We strongly recommend using the path suggested below.</strong>
|
|
<div class="input-group mt-2">
|
|
<input type="text" class="form-control" id="s5DeployerTargetInput"
|
|
placeholder="C:/Users/Admin/Desktop/server01/server.cfg" disabled>
|
|
<span class="input-group-append">
|
|
<button class="btn btn-sm btn-outline-dark" type="button" id="s5DeployerTargetChangeBtn"
|
|
onclick="s5DeployerTargetEnablePath()">Change Path</button>
|
|
</span>
|
|
</div>
|
|
<span id="s5DeployerTargetError" class="text-danger fade-in d-none"></span>
|
|
<div class="step-actions">
|
|
<button class="btn btn-pill btn-secondary previous-step"
|
|
type="button">Back</button>
|
|
<button class="btn btn-pill btn-info next-step" type="button"
|
|
data-feedback="validateLocalDeployPath" id="s5DeployerTargetSubmitBtn" autofocus>Save</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- input server cfg file -->
|
|
<div class="d-none" id="s5ServerCFGDiv">
|
|
The path to your server config file, usually named <code>server.cfg</code>. <br>
|
|
This can either be absolute, or relative to the Server Data folder.
|
|
<input type="text" class="form-control mt-2" id="s5ServerCFGInput"
|
|
placeholder="C:/Users/Admin/Desktop/server01/server.cfg">
|
|
<p class="text-success font-weight-bold mb-0 d-none" id="s5ServerCFGAutofill">
|
|
Config file detected! If this file is correct, just click on the save button below.
|
|
</p>
|
|
<span id="s5ServerCFGError" class="text-danger fade-in d-none"></span>
|
|
<div class="step-actions">
|
|
<div class="step-actions">
|
|
<button class="btn btn-pill btn-secondary previous-step"
|
|
type="button">Back</button>
|
|
<button class="btn btn-pill btn-info next-step" type="button"
|
|
data-feedback="validateCFGFile" id="s5ServerCFGSubmitBtn">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</li>
|
|
|
|
|
|
<!-- Finish -->
|
|
<li class="step">
|
|
<div class="step-title waves-effect">Finish</div>
|
|
<div class="step-content">
|
|
<h4 id="finishMessage">We are all set!</h4>
|
|
<span id="saveError" class="text-danger fade-in d-none"></span>
|
|
<div class="step-actions">
|
|
<button class="btn btn-pill btn-success next-step" type="button"
|
|
data-feedback="performSave" id="finishButton">
|
|
Save & Start Server</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<!-- CoreUI and necessary plugins-->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"
|
|
integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ=="
|
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/marked@4.0.16/lib/marked.umd.min.js"></script>
|
|
<script src="js/coreui.bundle.min.js?txVer=<%= txAdminVersion %>"></script>
|
|
<script src="js/bootstrap-notify.min.js?txVer=<%= txAdminVersion %>"></script>
|
|
<script src="js/txadmin/base.js?txVer=<%= txAdminVersion %>"></script>
|
|
|
|
<!-- JS -->
|
|
<script src="https://unpkg.com/materialize-stepper@3.1.0/dist/js/mstepper.js"></script>
|
|
<script>
|
|
//Constants
|
|
const recipesIndexURL = 'https://raw.githubusercontent.com/tabarra/txAdmin-recipes/main/indexv4.json';
|
|
const txDataPath = '<%= dataPath %>';
|
|
const forceGameName = '<%= forceGameName %>' || false;
|
|
const deployerEngineVersion = parseInt('<%= deployerEngineVersion %>');
|
|
const skipServerName = ('<%= skipServerName %>' === 'true');
|
|
const SPINNER_HTMLSized = `<div style="min-height: 250px; margin: auto;">${SPINNER_HTML}</div>`;
|
|
const connectErrorMessage = `<b>Unable to connect to txAdmin.</b> <br> If you closed it, please start again and retry.`;
|
|
|
|
//Vars
|
|
const suggestions = {};
|
|
let loadedTemplates = [];
|
|
let selectedTemplateIndex;
|
|
let selectedDeploymentType;
|
|
let deploymentID;
|
|
|
|
//Stepper config
|
|
const stepper = document.querySelector('.stepper');
|
|
const stepperInstace = new MStepper(stepper, {
|
|
autoFocusInput: true,
|
|
showFeedbackPreloader: true,
|
|
stepTitleNavigation: false,
|
|
firstActive: skipServerName ? 1 : 0,
|
|
feedbackPreloader: SPINNER_HTMLSized,
|
|
});
|
|
|
|
//Functions
|
|
const buildDeployName = (templateName) => {
|
|
const treatedName = templateName.replace(/[^a-zA-Z0-9\.\-_]/g, '');
|
|
const shortName = (treatedName.length < 3)? 'DumbName' : treatedName;
|
|
const timestamp = Math.round(Date.now()/1000).toString(16).padStart(6, 0).slice(-6).toUpperCase();
|
|
return {
|
|
id: `${shortName}_${timestamp}`,
|
|
path: `${txDataPath}/${shortName}_${timestamp}.base`,
|
|
};
|
|
}
|
|
const inputKeyUp = (event) => {
|
|
event.target.classList.remove('is-invalid', 'is-valid');
|
|
if (event.target.id == 's5ServerCFGInput'){
|
|
s5ServerCFGAutofill.classList.add('d-none');
|
|
}
|
|
if (event.keyCode === 13) {
|
|
event.preventDefault();
|
|
document.getElementById(event.target.id+'SubmitBtn').click();
|
|
}
|
|
}
|
|
const acceptFix = (event) => {
|
|
event.target.classList.add('d-none');
|
|
const targetName = event.target.dataset.target;
|
|
const targetElement = document.getElementById(targetName);
|
|
targetElement.classList.remove('is-invalid');
|
|
targetElement.classList.add('is-valid');
|
|
targetElement.value = suggestions[targetName] || '';
|
|
const currentSteps = stepperInstace.getSteps();
|
|
currentSteps.active.step.classList.remove('wrong');
|
|
currentSteps.active.step.classList.add('done');
|
|
}
|
|
|
|
|
|
//Grabs the elements and set the event handlers
|
|
const finishButton = document.getElementById("finishButton");
|
|
const finishMessage = document.getElementById("finishMessage");
|
|
const frmName = document.getElementById("frmName");
|
|
const frmNameError = document.getElementById("frmNameError");
|
|
frmName.addEventListener("keyup", inputKeyUp);
|
|
|
|
//Step 4
|
|
const s4Title = document.getElementById("s4Title");
|
|
const s4PopularTemplateDiv = document.getElementById("s4PopularTemplateDiv");
|
|
const s4PopularTemplateCardTemplate = document.getElementById("s4PopularTemplateCardTemplate");
|
|
const s4PopularTemplateRow = document.getElementById("s4PopularTemplateRow");
|
|
const s4RemoteTemplateDiv = document.getElementById("s4RemoteTemplateDiv");
|
|
const s4CustomTemplateDiv = document.getElementById("s4CustomTemplateDiv");
|
|
const s4LocalDataPathDiv = document.getElementById("s4LocalDataPathDiv");
|
|
|
|
const s4RemoteTemplateInput = document.getElementById("s4RemoteTemplateInput");
|
|
const s4RemoteTemplateInputError = document.getElementById("s4RemoteTemplateInputError");
|
|
s4RemoteTemplateInput.addEventListener("keyup", inputKeyUp);
|
|
|
|
const s4LocalDataPathInput = document.getElementById("s4LocalDataPathInput");
|
|
const s4LocalDataPathInputError = document.getElementById("s4LocalDataPathInputError");
|
|
const s4LocalDataPathInputFixBtn = document.getElementById("s4LocalDataPathInputFixBtn");
|
|
s4LocalDataPathInput.addEventListener("keyup", inputKeyUp);
|
|
s4LocalDataPathInputFixBtn.addEventListener("click", acceptFix);
|
|
|
|
//Step 5
|
|
const s5Title = document.getElementById("s5Title");
|
|
const s5Content = document.getElementById("s5Content");
|
|
const s5DeployerTargetDiv = document.getElementById("s5DeployerTargetDiv");
|
|
const s5ServerCFGDiv = document.getElementById("s5ServerCFGDiv");
|
|
|
|
const s5DeployerTargetChangeBtn = document.getElementById("s5DeployerTargetChangeBtn");
|
|
const s5DeployerTargetInput = document.getElementById("s5DeployerTargetInput");
|
|
const s5DeployerTargetError = document.getElementById("s5DeployerTargetError");
|
|
s5DeployerTargetInput.addEventListener("keyup", inputKeyUp);
|
|
|
|
const s5ServerCFGInput = document.getElementById("s5ServerCFGInput");
|
|
const s5ServerCFGAutofill = document.getElementById("s5ServerCFGAutofill");
|
|
const s5ServerCFGError = document.getElementById("s5ServerCFGError");
|
|
s5ServerCFGInput.addEventListener("keyup", inputKeyUp);
|
|
|
|
|
|
// Server Name step validation
|
|
function validateName(destroyFeedback) {
|
|
frmName.classList.remove('is-invalid', 'is-valid');
|
|
frmNameError.classList.add('d-none');
|
|
const name = frmName.value.trim();
|
|
|
|
if(name.length < 3 || name.length > 22){
|
|
frmNameError.innerHTML = `The name must have between 3 and 22 characters.`;
|
|
frmNameError.classList.remove('d-none');
|
|
frmName.classList.add('is-invalid');
|
|
stepperInstace.wrongStep();
|
|
destroyFeedback(false)
|
|
}else{
|
|
frmName.classList.add('is-valid');
|
|
destroyFeedback(true)
|
|
}
|
|
}
|
|
|
|
// Deployment type selection
|
|
function selectDeploymentType(depType){
|
|
console.log(`Selected ${depType}`);
|
|
selectedDeploymentType = depType;
|
|
selectedTemplateURL = null;
|
|
if(depType == 'popular' || depType == 'remote'){
|
|
s4Title.textContent = "Select Template";
|
|
s5Title.textContent = "Data Location";
|
|
finishButton.textContent = "Go to Recipe Deployer";
|
|
finishMessage.textContent = "We are almost there!";
|
|
s4PopularTemplateRow.innerHTML = SPINNER_HTMLSized;
|
|
s4CustomTemplateDiv.classList.add('d-none');
|
|
s4LocalDataPathDiv.classList.add('d-none');
|
|
s5DeployerTargetDiv.classList.remove('d-none');
|
|
s5ServerCFGDiv.classList.add('d-none');
|
|
|
|
if(depType == 'popular'){
|
|
s4PopularTemplateDiv.classList.remove('d-none');
|
|
s4RemoteTemplateDiv.classList.add('d-none');
|
|
setFavTemplatesCards();
|
|
}else{
|
|
s4PopularTemplateDiv.classList.add('d-none');
|
|
s4RemoteTemplateDiv.classList.remove('d-none');
|
|
}
|
|
|
|
}else if(depType == 'custom'){
|
|
const deployData = buildDeployName(frmName.value.trim());
|
|
s5DeployerTargetInput.value = deployData.path;
|
|
deploymentID = deployData.id;
|
|
|
|
s4Title.textContent = "Information";
|
|
s5Title.textContent = "Data Location";
|
|
finishButton.textContent = "Go to Recipe Deployer";
|
|
finishMessage.textContent = "We are almost there!";
|
|
s4CustomTemplateDiv.classList.remove('d-none');
|
|
s4RemoteTemplateDiv.classList.add('d-none');
|
|
s4PopularTemplateDiv.classList.add('d-none');
|
|
s4LocalDataPathDiv.classList.add('d-none');
|
|
s5DeployerTargetDiv.classList.remove('d-none');
|
|
s5ServerCFGDiv.classList.add('d-none');
|
|
|
|
}else if(depType == 'local'){
|
|
s4Title.textContent = "Existing Server Data";
|
|
s5Title.textContent = "Server CFG File";
|
|
finishButton.textContent = "Save & Start Server";
|
|
finishMessage.textContent = "We are all set!";
|
|
s4PopularTemplateDiv.classList.add('d-none');
|
|
s4CustomTemplateDiv.classList.add('d-none');
|
|
s4RemoteTemplateDiv.classList.add('d-none');
|
|
s4LocalDataPathDiv.classList.remove('d-none');
|
|
s5DeployerTargetDiv.classList.add('d-none');
|
|
s5ServerCFGDiv.classList.remove('d-none');
|
|
}else{
|
|
alert('Unknown deployment type!');
|
|
window.location.reload(true);
|
|
}
|
|
}
|
|
|
|
function tagToGlass(tag) {
|
|
if (tag === 'fivem') {
|
|
return 'recipeTagFivem';
|
|
} else if (tag === 'redm') {
|
|
return 'recipeTagRedm';
|
|
} else {
|
|
return 'recipeTagDefault';
|
|
}
|
|
}
|
|
|
|
//Download Popular Recipe list and create cards
|
|
async function setFavTemplatesCards(){
|
|
//NOTE: mocked
|
|
// const tmpGETResponse = [
|
|
// {
|
|
// "engine": 3,
|
|
// "name": "CFX Default",
|
|
// "author": "Tabarra",
|
|
// "version": "3.0.0",
|
|
// "description": "Recipe for the base resources required to run a minimal FiveM server.",
|
|
// "url": "https://raw.githubusercontent.com/tabarra/CFX-Default-recipe/main/recipe.yaml"
|
|
// },
|
|
// {
|
|
// "engine": 3,
|
|
// "name": "Plume ESX",
|
|
// "author": "Tabarra & Chip",
|
|
// "version": "2.0.0",
|
|
// "description": "A full featured (13 jobs) and highly configurable yet lightweight ESX v1-final base that can be easily extendable.",
|
|
// "url": "https://raw.githubusercontent.com/tabarra/PlumeESX-recipe/master/recipe.yaml"
|
|
// }
|
|
// ];
|
|
s4PopularTemplateRow.innerText = `Loading templates...`;
|
|
loadedTemplates = [];
|
|
$.ajax({
|
|
type: "GET",
|
|
url: recipesIndexURL,
|
|
dataType: 'json',
|
|
success: function (templates) {
|
|
if(!Array.isArray(templates)){
|
|
s4PopularTemplateRow.innerText = `Failed to load recipe list.
|
|
Please click on the "back" button and try again.`;
|
|
}else{
|
|
loadedTemplates = templates;
|
|
// loadedTemplates = tmpGETResponse;
|
|
s4PopularTemplateRow.innerHTML = '';
|
|
let hasIncompatible = false;
|
|
loadedTemplates.forEach((recipe, key) => {
|
|
//Filtering for forceGameName
|
|
if (
|
|
forceGameName
|
|
&& Array.isArray(recipe.tags)
|
|
&& !recipe.tags.includes(forceGameName)
|
|
) return;
|
|
|
|
const newNode = document.createElement("div");
|
|
newNode.innerHTML = s4PopularTemplateCardTemplate.innerHTML;
|
|
// newNode.classList.add('col-12', 'col-md-6', 'p-0'); //2 per line
|
|
newNode.classList.add('col-8');
|
|
newNode.querySelector('.titleText > strong').innerText = recipe.name;
|
|
newNode.querySelector('.description').innerText = recipe.description;
|
|
if(recipe.engine !== deployerEngineVersion){
|
|
hasIncompatible = true;
|
|
newNode.querySelector('.bigbutton').classList.add('disabled');
|
|
const newTag = document.createElement("span");
|
|
newTag.classList.add('recipeTag', 'recipeTagDefault')
|
|
newTag.textContent = 'INCOMPATIBLE';
|
|
newNode.querySelector('.tagList').appendChild(newTag);
|
|
}else{
|
|
newNode.querySelector('.bigbutton').onclick = () => {selectTemplate(key)};
|
|
if (Array.isArray(recipe.tags)) {
|
|
recipe.tags.forEach(tag => {
|
|
const newTag = document.createElement("span");
|
|
newTag.classList.add('recipeTag', tagToGlass(tag))
|
|
newTag.textContent = (tag || '').toUpperCase();
|
|
newNode.querySelector('.tagList').appendChild(newTag);
|
|
})
|
|
}
|
|
}
|
|
s4PopularTemplateRow.appendChild(newNode);
|
|
});
|
|
if(hasIncompatible){
|
|
const warningNode = document.createElement("span");
|
|
warningNode.innerHTML = `Some recipes are not compatible with your txAdmin version.<br> Please update txAdmin to be able to use them.`;
|
|
warningNode.classList.add('text-danger', 'font-weight-bold', 'ml-3');
|
|
s4PopularTemplateRow.appendChild(warningNode);
|
|
}
|
|
}
|
|
},
|
|
error: function (xmlhttprequest, textstatus, message) {
|
|
s4PopularTemplateRow.innerText = `Failed to load recipe list.
|
|
Please click on the "back" button and try again.`;
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
// Deployment type selection
|
|
function selectTemplate(key){
|
|
selectedTemplateIndex = key;
|
|
if(!loadedTemplates[key]){
|
|
return alert('Invalid Template');
|
|
}
|
|
const deployData = buildDeployName(loadedTemplates[key].name);
|
|
s5DeployerTargetInput.value = deployData.path;
|
|
deploymentID = deployData.id;
|
|
s5DeployerTargetInput.disabled = true;
|
|
s5DeployerTargetChangeBtn.classList.remove('d-none');
|
|
stepperInstace.nextStep();
|
|
}
|
|
|
|
// Validate a remote template url
|
|
function validateRemoteTemplate(destroyFeedback) {
|
|
const currentSteps = stepperInstace.getSteps();
|
|
s4RemoteTemplateInput.classList.remove('is-invalid', 'is-valid');
|
|
s4RemoteTemplateInputError.classList.add('d-none');
|
|
|
|
txAdminAPI({
|
|
type: "POST",
|
|
url: '/setup/validateRecipeURL',
|
|
timeout: REQ_TIMEOUT_LONG,
|
|
data: {recipeURL: s4RemoteTemplateInput.value.trim()},
|
|
success: function (data) {
|
|
if (checkApiLogoutRefresh(data)) return;
|
|
if (data.success == true){
|
|
const deployData = buildDeployName(data.name);
|
|
s5DeployerTargetInput.value = deployData.path;
|
|
deploymentID = deployData.id;
|
|
currentSteps.active.step.classList.remove('wrong');
|
|
currentSteps.active.step.classList.add('done');
|
|
s4RemoteTemplateInput.classList.add('is-valid');
|
|
destroyFeedback(true);
|
|
}else{
|
|
stepperInstace.wrongStep();
|
|
s4RemoteTemplateInputError.innerHTML = `<br>Error:</b> ${data.message} <br>Make sure this is a valid Recipe URL.`;
|
|
s4RemoteTemplateInputError.classList.remove('d-none');
|
|
s4RemoteTemplateInput.classList.add('is-invalid');
|
|
destroyFeedback(false);
|
|
}
|
|
},
|
|
error: function (xmlhttprequest, textstatus, message) {
|
|
stepperInstace.wrongStep();
|
|
s4RemoteTemplateInputError.innerHTML = `<br>Error:</b> ${message} <br>Make sure this is a valid Recipe URL.`;
|
|
s4RemoteTemplateInputError.classList.remove('d-none');
|
|
s4RemoteTemplateInput.classList.add('is-invalid');
|
|
destroyFeedback(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
//Local deploy path enable
|
|
function s5DeployerTargetEnablePath(){
|
|
s5DeployerTargetInput.disabled = false;
|
|
s5DeployerTargetInput.focus();
|
|
s5DeployerTargetChangeBtn.classList.add('d-none');
|
|
}
|
|
|
|
|
|
// Validate a local deploy path
|
|
function validateLocalDeployPath(destroyFeedback) {
|
|
const currentSteps = stepperInstace.getSteps();
|
|
s5DeployerTargetInput.classList.remove('is-invalid', 'is-valid');
|
|
s5DeployerTargetError.classList.add('d-none');
|
|
|
|
txAdminAPI({
|
|
type: "POST",
|
|
url: '/setup/validateLocalDeployPath',
|
|
timeout: REQ_TIMEOUT_LONG,
|
|
data: {deployPath: s5DeployerTargetInput.value.trim()},
|
|
success: function (data) {
|
|
if (checkApiLogoutRefresh(data)) return;
|
|
if (data.success == true){
|
|
currentSteps.active.step.classList.remove('wrong');
|
|
currentSteps.active.step.classList.add('done');
|
|
s5DeployerTargetInput.classList.add('is-valid');
|
|
destroyFeedback(true);
|
|
}else{
|
|
stepperInstace.wrongStep();
|
|
s5DeployerTargetError.innerHTML = `<br>Error:</b> ${data.message}`;
|
|
s5DeployerTargetError.classList.remove('d-none');
|
|
s5DeployerTargetInput.classList.add('is-invalid');
|
|
destroyFeedback(false);
|
|
}
|
|
},
|
|
error: function (xmlhttprequest, textstatus, message) {
|
|
stepperInstace.wrongStep();
|
|
s5DeployerTargetError.innerHTML = connectErrorMessage;
|
|
s5DeployerTargetError.classList.remove('d-none');
|
|
s5DeployerTargetInput.classList.add('is-invalid');
|
|
destroyFeedback(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// Server Data Folder step validation
|
|
function validateLocalDataFolder(destroyFeedback) {
|
|
const currentSteps = stepperInstace.getSteps();
|
|
s4LocalDataPathInput.classList.remove('is-invalid', 'is-valid');
|
|
s4LocalDataPathInputError.classList.add('d-none');
|
|
s5ServerCFGAutofill.classList.add('d-none');
|
|
s5ServerCFGError.classList.add('d-none');
|
|
s5ServerCFGInput.value = '';
|
|
|
|
txAdminAPI({
|
|
type: "POST",
|
|
url: '/setup/validateLocalDataFolder',
|
|
timeout: REQ_TIMEOUT_LONG,
|
|
data: {dataFolder: s4LocalDataPathInput.value.trim()},
|
|
success: function (data) {
|
|
if (checkApiLogoutRefresh(data)) return;
|
|
if (data.success == true){
|
|
currentSteps.active.step.classList.remove('wrong');
|
|
currentSteps.active.step.classList.add('done');
|
|
s4LocalDataPathInput.classList.add('is-valid');
|
|
if(data.detectedConfig){
|
|
s5ServerCFGAutofill.classList.remove('d-none');
|
|
s5ServerCFGInput.value = data.detectedConfig;
|
|
}
|
|
destroyFeedback(true)
|
|
}else{
|
|
s4LocalDataPathInputError.innerHTML = data.message;
|
|
s4LocalDataPathInputError.classList.remove('d-none');
|
|
s4LocalDataPathInput.classList.add('is-invalid');
|
|
stepperInstace.wrongStep();
|
|
if(data.suggestion){
|
|
suggestions['s4LocalDataPathInput'] = data.suggestion;
|
|
s4LocalDataPathInputFixBtn.classList.remove('d-none');
|
|
}
|
|
destroyFeedback(false)
|
|
}
|
|
},
|
|
error: function (xmlhttprequest, textstatus, message) {
|
|
s4LocalDataPathInputError.innerHTML = connectErrorMessage;
|
|
s4LocalDataPathInputError.classList.remove('d-none');
|
|
destroyFeedback(false)
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
// Server CFG File step validation
|
|
function validateCFGFile(destroyFeedback) {
|
|
const currentSteps = stepperInstace.getSteps();
|
|
s5ServerCFGInput.classList.remove('is-invalid', 'is-valid');
|
|
s5ServerCFGError.classList.add('d-none');
|
|
const outData = {
|
|
template: false,
|
|
dataFolder: s4LocalDataPathInput.value.trim(),
|
|
cfgFile: s5ServerCFGInput.value.trim(),
|
|
};
|
|
|
|
txAdminAPI({
|
|
type: "POST",
|
|
url: '/setup/validateCFGFile',
|
|
timeout: REQ_TIMEOUT_LONG,
|
|
data: outData,
|
|
success: function (data) {
|
|
if (checkApiLogoutRefresh(data)) return;
|
|
if (data.success == true){
|
|
currentSteps.active.step.classList.remove('wrong');
|
|
currentSteps.active.step.classList.add('done');
|
|
frmName.classList.add('is-valid');
|
|
destroyFeedback(true)
|
|
}else{
|
|
s5ServerCFGError.innerHTML = convertMarkdown(data.message, true);
|
|
s5ServerCFGError.classList.remove('d-none');
|
|
s5ServerCFGInput.classList.add('is-invalid');
|
|
stepperInstace.wrongStep();
|
|
destroyFeedback(false)
|
|
}
|
|
},
|
|
error: function (xmlhttprequest, textstatus, message) {
|
|
s5ServerCFGError.innerHTML = connectErrorMessage;
|
|
s5ServerCFGError.classList.remove('d-none');
|
|
destroyFeedback(false)
|
|
}
|
|
});
|
|
}
|
|
|
|
// Save function
|
|
function performSave(destroyFeedback) {
|
|
const saveError = document.getElementById("saveError");
|
|
saveError.classList.add('d-none');
|
|
|
|
const outData = {
|
|
name: frmName.value.trim(),
|
|
type: selectedDeploymentType
|
|
};
|
|
if(selectedDeploymentType == 'popular'){
|
|
outData.isTrustedSource = true;
|
|
outData.recipeURL = loadedTemplates[selectedTemplateIndex].url;
|
|
outData.targetPath = s5DeployerTargetInput.value.trim();
|
|
outData.deploymentID = deploymentID;
|
|
|
|
}else if(selectedDeploymentType == 'remote'){
|
|
outData.isTrustedSource = false;
|
|
outData.recipeURL = s4RemoteTemplateInput.value.trim();
|
|
outData.targetPath = s5DeployerTargetInput.value.trim();
|
|
outData.deploymentID = deploymentID;
|
|
|
|
}else if(selectedDeploymentType == 'custom'){
|
|
outData.targetPath = s5DeployerTargetInput.value.trim();
|
|
outData.deploymentID = deploymentID;
|
|
|
|
}else if(selectedDeploymentType == 'local'){
|
|
outData.dataFolder = s4LocalDataPathInput.value.trim();
|
|
outData.cfgFile = s5ServerCFGInput.value.trim();
|
|
|
|
}else{
|
|
return alert('Unknown deployment type')
|
|
}
|
|
|
|
txAdminAPI({
|
|
type: "POST",
|
|
url: '/setup/save',
|
|
timeout: REQ_TIMEOUT_REALLY_LONG,
|
|
data: outData,
|
|
success: function (data) {
|
|
if (checkApiLogoutRefresh(data)) return;
|
|
if (data.success == true){
|
|
navigateParentTo((selectedDeploymentType === 'local') ? '/server/console' : '/server/deployer');
|
|
}else{
|
|
saveError.innerHTML = convertMarkdown(`${data.message}\nPlease refresh the page and start again.`, true);
|
|
saveError.classList.remove('d-none');
|
|
stepperInstace.wrongStep();
|
|
destroyFeedback(false)
|
|
}
|
|
},
|
|
error: function (xmlhttprequest, textstatus, message) {
|
|
saveError.innerHTML = connectErrorMessage;
|
|
saveError.classList.remove('d-none');
|
|
destroyFeedback(false)
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|