feat: add VNC check, SSH key paste, resources panel, sliders, and self-service billing
- VNC panel auto-hides when VNC is disabled on the server - SSH key paste textarea at checkout with API key creation during provisioning - Resources panel with current allocation, traffic progress bar, and upgrade link - changePackage() now applies individual resource modifications from configurable options - Order form configurable option dropdowns replaced with styled range sliders - Self-service billing: credit balance, usage breakdown, credit top-up from client area - Self-service config options (mode, auto top-off threshold/amount) on products - Auto top-off via WHMCS cron when credit falls below threshold - CHANGELOG.md covering all versions from 0.0.6 to present Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -173,6 +173,30 @@ add_hook('ClientAreaFooterOutput', 1, function ($vars) {
|
||||
|
||||
// Handle SSH keys
|
||||
if (sshInputField) {
|
||||
// Create the paste-key textarea (hidden initially if keys exist)
|
||||
var sshPasteContainer = document.createElement('div');
|
||||
sshPasteContainer.setAttribute('id', 'vf-ssh-paste-container');
|
||||
sshPasteContainer.style.display = 'none';
|
||||
sshPasteContainer.style.marginTop = '8px';
|
||||
|
||||
var pasteLabel = document.createElement('label');
|
||||
pasteLabel.textContent = 'Paste your SSH public key:';
|
||||
pasteLabel.style.display = 'block';
|
||||
pasteLabel.style.marginBottom = '4px';
|
||||
|
||||
var pasteArea = document.createElement('textarea');
|
||||
pasteArea.className = 'form-control';
|
||||
pasteArea.setAttribute('id', 'vf-ssh-paste');
|
||||
pasteArea.setAttribute('rows', '3');
|
||||
pasteArea.setAttribute('placeholder', 'ssh-rsa AAAA... or ssh-ed25519 AAAA...');
|
||||
|
||||
pasteArea.addEventListener('input', function() {
|
||||
sshInputField.value = this.value.trim();
|
||||
});
|
||||
|
||||
sshPasteContainer.appendChild(pasteLabel);
|
||||
sshPasteContainer.appendChild(pasteArea);
|
||||
|
||||
if (sshKeys.length > 0) {
|
||||
var sshSelect = document.createElement('select');
|
||||
sshSelect.className = 'form-control';
|
||||
@@ -190,20 +214,102 @@ add_hook('ClientAreaFooterOutput', 1, function ($vars) {
|
||||
sshSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Add new key option
|
||||
var addNewOption = document.createElement('option');
|
||||
addNewOption.value = '__new__';
|
||||
addNewOption.text = 'Add new key...';
|
||||
sshSelect.appendChild(addNewOption);
|
||||
|
||||
sshSelect.addEventListener('change', function() {
|
||||
sshInputField.value = this.value;
|
||||
if (this.value === '__new__') {
|
||||
sshPasteContainer.style.display = 'block';
|
||||
sshInputField.value = '';
|
||||
} else {
|
||||
sshPasteContainer.style.display = 'none';
|
||||
document.getElementById('vf-ssh-paste').value = '';
|
||||
sshInputField.value = this.value;
|
||||
}
|
||||
});
|
||||
|
||||
sshInputField.parentNode.insertBefore(sshSelect, sshInputField.nextSibling);
|
||||
sshSelect.parentNode.insertBefore(sshPasteContainer, sshSelect.nextSibling);
|
||||
sshInputField.style.display = 'none';
|
||||
} else {
|
||||
// No existing keys — show the paste textarea directly
|
||||
sshPasteContainer.style.display = 'block';
|
||||
sshInputField.parentNode.insertBefore(sshPasteContainer, sshInputField.nextSibling);
|
||||
sshInputField.style.display = 'none';
|
||||
if (sshInputLabel) sshInputLabel.style.display = 'none';
|
||||
// Also hide the parent container if it exists
|
||||
var sshContainer = sshInputField.closest('.form-group');
|
||||
if (sshContainer) sshContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Slider UI: enhance known configurable option selects with range sliders
|
||||
var sliderResourceNames = ['Memory', 'CPU Cores', 'Storage', 'Bandwidth', 'Inbound Network Speed', 'Outbound Network Speed'];
|
||||
var sliderUnits = {
|
||||
'Memory': 'MB', 'CPU Cores': 'Core(s)', 'Storage': 'GB',
|
||||
'Bandwidth': 'GB', 'Inbound Network Speed': 'Mbps', 'Outbound Network Speed': 'Mbps'
|
||||
};
|
||||
|
||||
var configSelects = document.querySelectorAll('select[name^=\"configoption[\"]');
|
||||
configSelects.forEach(function(sel) {
|
||||
// Find the label for this select
|
||||
var label = null;
|
||||
var labelEl = sel.closest('.form-group, .row');
|
||||
if (labelEl) {
|
||||
label = labelEl.querySelector('label');
|
||||
}
|
||||
if (!label) return;
|
||||
|
||||
var labelText = label.textContent.trim();
|
||||
var matchedResource = null;
|
||||
sliderResourceNames.forEach(function(name) {
|
||||
if (labelText.indexOf(name) !== -1) {
|
||||
matchedResource = name;
|
||||
}
|
||||
});
|
||||
if (!matchedResource) return;
|
||||
|
||||
var options = [];
|
||||
for (var i = 0; i < sel.options.length; i++) {
|
||||
options.push({
|
||||
value: sel.options[i].value,
|
||||
label: sel.options[i].text
|
||||
});
|
||||
}
|
||||
if (options.length < 2) return;
|
||||
|
||||
var unit = sliderUnits[matchedResource] || '';
|
||||
|
||||
// Create slider container
|
||||
var container = document.createElement('div');
|
||||
container.className = 'vf-slider-container';
|
||||
|
||||
var valueDisplay = document.createElement('div');
|
||||
valueDisplay.className = 'vf-slider-value';
|
||||
valueDisplay.textContent = options[sel.selectedIndex || 0].label + (unit ? ' ' + unit : '');
|
||||
|
||||
var slider = document.createElement('input');
|
||||
slider.type = 'range';
|
||||
slider.className = 'vf-slider form-range';
|
||||
slider.min = '0';
|
||||
slider.max = String(options.length - 1);
|
||||
slider.step = '1';
|
||||
slider.value = String(sel.selectedIndex || 0);
|
||||
|
||||
slider.addEventListener('input', function() {
|
||||
var idx = parseInt(this.value);
|
||||
sel.selectedIndex = idx;
|
||||
valueDisplay.textContent = options[idx].label + (unit ? ' ' + unit : '');
|
||||
// Trigger change event on hidden select for WHMCS pricing
|
||||
var evt = new Event('change', { bubbles: true });
|
||||
sel.dispatchEvent(evt);
|
||||
});
|
||||
|
||||
container.appendChild(valueDisplay);
|
||||
container.appendChild(slider);
|
||||
|
||||
sel.parentNode.insertBefore(container, sel.nextSibling);
|
||||
sel.style.display = 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
";
|
||||
|
||||
Reference in New Issue
Block a user