openpanel/templates/admini/pm2.html

536 lines
20 KiB
HTML

{% extends 'base.html' %}
{% block content %}
{% if domains %}
<style>
@keyframes rotate {
to {
--angle: 360deg
}
}
@property --angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false
}
.CrawlerStatusCard.active {
animation: rotate 2s linear infinite;
background: hsla(0,0%,100%,.5);
border: 2px solid transparent;
border-image: conic-gradient(from var(--angle),transparent 0deg 90deg,transparent 90deg 180deg,transparent 180deg 270deg,#0077bc 270deg 1turn) 1 stretch;
}
</style>
<div class="modal fade" id="viewModal" tabindex="-1" role="dialog" aria-labelledby="viewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="viewModalLabel">{{ _('Application Logs') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ _('Close') }}"></button>
</div>
<div class="modal-body bg-dark">
<div id="fileContent" class="mb-0"></div>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function() {
$("#port").on('input', function() {
// Get the user-input port value
var portValue = $(this).val();
// Make an AJAX request to check if the port is in use
$.ajax({
url: "/json/check_if_port_is_in_use",
type: "POST",
data: {port: portValue},
success: function(data) {
// Display the appropriate symbol and message based on the response
var resultDiv = $("#port-check-result");
if (data.message.includes("not in use")) {
resultDiv.html('<i class="bi bi-check-lg" style="color:green;"></i> {{ _("available") }}');
} else {
resultDiv.html('<i class="bi bi-x-lg" style="color:red;"></i> {{ _("already in use") }}');
}
},
error: function(data) {
// Handle the error here if needed
console.log("Error:", data);
}
});
});
$("#startup_file").on('input', function() {
// Get the user-input file value
var fileValue = $(this).val();
// Make an AJAX request to check if the file is in use
$.ajax({
url: "/json/check_if_file_exists",
type: "POST",
data: {file: fileValue},
success: function(data) {
// Display the appropriate symbol and message based on the response
var resultDiv = $("#file-check-result");
if (data.message.includes("exists")) {
resultDiv.html('<i class="bi bi-check-lg" style="color:green;"></i> {{ _("file exists") }}');
} else {
resultDiv.html('<i class="bi bi-x-lg" style="color:red;"></i> {{ _("file does not exist!") }}');
}
},
error: function(data) {
// Handle the error here if needed
console.log("Error:", data);
}
});
});
});
</script>
<div class="collapse" id="collapseExample">
<div id="toAddActive" class="card CrawlerStatusCard card-body">
<!-- Form for adding new applications -->
<div class="container">
<a href="#" class="nije-link" id="cancelLink" style="display: none;"><i class="bi bi-x-lg" style="right: 15px;top: 15px;position: absolute;color: black;padding: 6px 10px;border-radius: 50px;"></i></a>
<div class="row">
<div class="col-md-6 offset-md-3">
<h2 class="mb-3"><i class="bi bi-app-indicator"></i> {{ _('Create a new Application') }}</h2>
<p>{{ _("Run a NodeJS or Python application.") }}</p>
<form method="post" id="installpm2" action="/pm2/install">
<div class="form-group mb-2">
<label for="domain_id">{{ _('Application URL:') }}</label>
<div class="input-group">
<select class="form-control" name="domain_id" id="domain_id">
{% for domain in domains %}
<option class="punycode" value="{{ domain.domain_id }}">{{ domain.domain_url }}</option>
{% endfor %}
</select>
<div class="input-group-append">
<input type="text" class="form-control" name="subdirectory" id="subdirectory" placeholder="{{ _('subfolder (optional)') }}">
</div>
</div>
</div>
<div class="form-group">
<label for="admin_email">{{ _('Application Startup file:') }}</label>
<div class="input-group mb-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text" id="full-path">/home/{{ current_username }}/</span>
</div>
<input type="text" class="form-control" name="startup_file" id="startup_file" required="">
</div>
<div id="file-check-result">&nbsp;</div>
</div>
</div>
<div class="form-group">
<label for="flags">{{ _('Optional flags:') }}</label>
<div class="input-group mb-0">
<input type="text" class="form-control" name="flags" id="flags">
</div>
</div>
<div class="form-group row mt-2">
<div class="col-md-6">
<label for="app_type">{{ _('Type:') }}</label>
<select class="form-select select2" name="app_type" id="app_type">
<option value="node">{{ _("NodeJS") }}</option>
<option value="python3">{{ _("Python") }}</option>
</select>
</div>
<!--div class="col-md-6">
<label for="version">Version:</label>
<input type="text" class="form-control" name="version" id="version" value="">
</div-->
<div class="col-md-6">
<label for="website_name">{{ _("Port:") }}</label>
<input type="text" class="form-control" name="port" id="port" value="" required>
<div id="port-check-result">&nbsp;</div>
</div>
</div>
<div class="form-group row mt-2 mb-2">
<div class="col-md-6">
<label for="watch">{{ _('Watch:') }}</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckChecked" checked>
<label class="form-check-label" for="flexSwitchCheckChecked">{{ _('Automatically restart app on file changes') }}</label>
</div>
</div>
<div class="col-md-6">
<label for="logs">{{ _('Enable logs:') }}</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckChecked" checked>
<label class="form-check-label" for="flexSwitchCheckChecked">{{ _('Collect logs') }}</label>
</div>
</div>
</div>
<script>
$(document).ready(function() {
// Check if the URL contains the parameter "install"
const urlParams = new URLSearchParams(window.location.search);
const installParam = urlParams.get('install');
if (installParam || window.location.hash === '#install') {
// Show the Bootstrap collapsible element
$("#collapseExample").collapse('show');
}
// Add event listener to the "Install website" button to toggle form and jumbotron
$("#collapseExample").on('shown.bs.collapse', function () {
$("#jumbotronSection").hide();
$("#cancelLink").show();
});
// Add event listener to the "Cancel" link to toggle form and jumbotron
$("#cancelLink").click(function() {
$("#collapseExample").collapse('hide');
$("#jumbotronSection").show();
$("#cancelLink").hide();
});
});
</script>
<br>
<button type="submit" class="btn btn-primary" id="installpm2Button">{{ _('Create') }}</button>
</form>
<div id="statusMessage"></div>
<script>
document.getElementById("installpm2").addEventListener("submit", function(event) {
event.preventDefault();
// Hide the installation form
const installForm = document.getElementById("installpm2");
installForm.style.display = "none"; // Hide the form
// Show the active state
document.getElementById("toAddActive").classList.add("active");
// Disable the button to prevent multiple clicks
const button = document.getElementById("installpm2Button");
button.disabled = true;
button.innerText = "Installing..."; // Initial message
// Clear previous status messages
const statusMessageDiv = document.getElementById("statusMessage");
statusMessageDiv.innerText = "";
// Start fetching for updates
const formData = new FormData(this); // Get the form data
const responseStream = fetch('/pm2/install', {
method: 'POST',
body: formData,
});
responseStream
.then(response => {
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
function read() {
reader.read().then(({ done, value }) => {
if (done) {
showCompletion();
return;
}
const message = decoder.decode(value, { stream: true });
const jsonMessages = message.split('\n').filter(Boolean); // Split lines and remove empty
jsonMessages.forEach(jsonMessage => {
try {
const data = JSON.parse(jsonMessage);
if (data.status) {
statusMessageDiv.innerText += data.status + "\n"; // append
} else if (data.error) {
toaster({
body: data.error,
className: 'border-0 text-white bg-danger',
});
showCompletion(); // redirect
}
} catch (e) {
//console.error("Error parsing JSON:", e);
}
});
read(); // Continue
});
}
read(); // Start
})
.catch(error => {
statusMessageDiv.innerText += "Error occurred while processing.\n";
console.error("Fetch error:", error);
toaster({
body: error,
className: 'border-0 text-white bg-danger',
});
showCompletion(); // redirect
});
function showCompletion() {
statusMessageDiv.style.display = "block";
window.location.href = '/pm2';
}
});
</script>
</div>
</div>
</div>
</div></div>
{% if pm2_data %}
<div class="table-responsive">
<table class="table table-bordered">
<thead class="table-dark">
<tr>
<!--th>ID</th-->
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Application / Domain Name.') }}">{{ _('Application') }}</span></th>
<!--th><span data-bs-toggle="tooltip" data-bs-placement="top" title="NodeJS/Python version the application is using.">Version</span></th-->
<th>{{ _('Uptime') }}</th>
<th>{{ _('Restarts') }}</th>
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Current status of the application process.') }}">{{ _('Status') }}</span></th>
<th>{{ _('CPU') }}</th>
<th>{{ _('Memory') }}</th>
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('If watching is enabled, the application will automatically restart when its files are edited.') }}">{{ _('Watching') }}</span></th>
<th>Actions</th>
<!-- New columns for data from the database -->
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="NodeJS/Python">{{ _('Type') }}</span></th>
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Port that the application is using.') }}">Port</span></th>
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Path to the application startup file (app.js)') }}"> {{ _('Startup file') }}</span></th>
</tr>
</thead>
<tbody>
{% if pm2_data %}
{% for row in pm2_data %}
<tr>
<!--td>{{ row[0] }}</td-->
<td>{{ row[1] }}</td>
<!--td>{{ row[3] }}</tdVERSION-->
<td>{{ row[6] }}</td>
<td>{{ row[7] }}</td>
<td class="{% if row[8] == 'stopped' %}text-danger{% elif row[8] == 'online' %}text-success{% else %}text-warning{% endif %}">
{{ row[8] }}
</td>
<td>{{ row[9] }}</td>
<td>{{ row[10] }}</td>
<td>{{ row[12] }}</td>
<td>
{% if row[8] == 'stopped' %}
<form action="/pm2/start/{{ row[1] }}" method="post" style="display: inline;">
<button type="submit" class="btn btn-success btn-sm"><i class="bi bi-play-fill"></i> {{ _('Start') }}</button>
</form>
{% else %}
<form action="/pm2/stop/{{ row[1] }}" method="post" style="display: inline;">
<button type="submit" class="btn btn-warning btn-sm"><i class="bi bi-stop-fill"></i> {{ _('Stop') }}</button>
</form>
{% endif %}
<form action="/pm2/restart/{{ row[1] }}" method="post" style="display: inline;">
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-arrow-clockwise"></i> {{ _('Restart') }}</button>
</form>
<form action="/pm2/delete/{{ row[1] }}" method="post" style="display: inline;">
<button class="btn btn-danger btn-sm" type="button" style="display: inline;" onclick="confirmAppDelete(this);"><i class="bi bi-trash3"></i> {{ _('Delete') }}</button>
</form>
<button class="btn btn-sm btn-transparent view-button" data-file="{{ row[1] }}" data-type="logs">{{ _('Logs') }}</button>
</td>
<!-- Data from the database for the corresponding site -->
{% for site in all_sites %}
{% if row[1] == site.site_name %}
<td>
{% if site.type == "NodeJS" %}
<i class=""><img src="/static/images/icons/nodejs.png" style="height: 25px;"></i>
{% elif site.type == "Python" %}
<i class=""><img src="/static/images/icons/python.png" style="height: 25px;"></i>
{% else %}
{{ site.type }}
{% endif %}
</td>
<td>{{ site.ports }}</td>
<td>{{ site.path }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="10">{{ _('No PM2 data available.') }}</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{% else %}
<!-- Display jumbotron for no installations -->
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1>{{ _('No existing Applications') }}</h1>
<p>{{ _('There are no existing applications. You can start a new NodeJS or Python app below.') }}</p>
<button class="btn btn-lg btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
<i class="bi bi-app-indicator"></i>{{ _(' Create Application') }}
</button>
</div>
{% endif %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/punycode/2.1.1/punycode.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
var punycodeElements = document.querySelectorAll(".punycode");
punycodeElements.forEach(function(element) {
element.textContent = punycode.toUnicode(element.textContent);
});
});
</script>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Status">
</div>
<div class="ms-auto" role="group" aria-label="Actions">
<button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
<i class="bi bi-plus-lg"></i> {{ _('New') }}<span class="desktop-only"> {{ _('Application') }}</span>
</button>
</div>
</footer>
<script>
function confirmAppDelete(button) {
var countdown = 5;
var countdownActive = true; // Variable to track countdown status
// Change the button style and text
$(button).removeClass('btn-danger').addClass('btn-dark').html('<i class="bi bi-trash3-fill"></i> Confirm <span class="btn-indicator btn-indicator-mini bg-danger">' + countdown + '</span>');
// Interval to update countdown
var intervalId = setInterval(function () {
countdown--;
// Update the countdown value in the button text
$(button).find('.btn-indicator-mini').text(countdown);
// Remove the onclick event to prevent further changes on subsequent clicks
$(button).removeAttr('onclick');
// If countdown reaches 0, revert the button, clear the interval, and set countdownActive to false
if (countdown === 0) {
clearInterval(intervalId);
revertButton(button);
countdownActive = false;
}
}, 1000);
// Add a click event to the confirm button
$(button).on('click', function () {
// Check if countdown is active before allowing form submission
if (countdownActive) {
// Submit the parent form when the button is clicked during the countdown
$(button).closest('form').submit();
}
});
}
// Function to revert the button to its initial state
function revertButton(button) {
$(button).removeClass('btn-dark').addClass('btn-danger').html('<i class="bi bi-trash3"></i> Delete');
$(button).attr('onclick', 'confirmAppDelete(this);');
}
</script>
<script>
$('html').on('click', '.view-button', function() {
const button = $(this);
const domain = button.data('file');
fetch(`/pm2/logs/${encodeURIComponent(domain)}`)
.then(response => response.text())
.then(data => {
const modalTitle = $('#viewModalLabel');
const modalBody = $('#viewModal').find('.modal-body');
modalTitle.text("Error log for application: " + domain);
modalBody.empty();
const textContent = document.createElement('pre');
textContent.textContent = data;
modalBody.append(textContent);
$('#viewModal').modal('show');
})
.catch(error => {
console.error('{{ _("Error fetching pm2 logs for applicaiton:") }}', error);
});
});
</script>
{% else %}
<!-- Display jumbotron for no domains -->
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1>{{ _("No Domains") }}</h1>
<p>{{ _("Add a domain name first in order to install Python adn NodeJS applications.") }}</p>
<a href="/domains#add-new" class="btn btn-lg btn-primary">
<i class="bi bi-plus-lg"></i> {{ _("Add a Domain Name") }}
</a>
</div>
{% endif %}
{% endblock %}