Added outline server managment page template

This commit is contained in:
Ultradesu
2025-07-21 17:15:35 +03:00
parent 90001a1d1e
commit a75d55ac9d
9 changed files with 1411 additions and 366 deletions

View File

@@ -92,6 +92,152 @@
white-space: nowrap;
}
/* Server admin compact styles */
.server-stats {
max-width: 120px;
min-width: 90px;
}
.server-activity {
max-width: 140px;
min-width: 100px;
}
.server-status {
max-width: 160px;
min-width: 120px;
}
.server-comment {
max-width: 200px;
min-width: 100px;
word-wrap: break-word;
}
/* Compact server display elements */
.changelist-results .server-stats div,
.changelist-results .server-activity div,
.changelist-results .server-status div {
line-height: 1.3;
margin: 1px 0;
}
/* Status indicator colors */
.status-online {
color: #16a34a !important;
font-weight: bold;
}
.status-error {
color: #dc2626 !important;
font-weight: bold;
}
.status-warning {
color: #f97316 !important;
font-weight: bold;
}
.status-unavailable {
color: #f97316 !important;
font-weight: bold;
}
/* Activity indicators */
.activity-high {
color: #16a34a !important;
}
.activity-medium {
color: #eab308 !important;
}
.activity-low {
color: #f97316 !important;
}
.activity-none {
color: #dc2626 !important;
}
/* User stats indicators */
.users-active {
color: #16a34a !important;
}
.users-medium {
color: #eab308 !important;
}
.users-low {
color: #f97316 !important;
}
.users-none {
color: #9ca3af !important;
}
/* Table cell width constraints for better layout */
table.changelist-results th:nth-child(1), /* Name */
table.changelist-results td:nth-child(1) {
width: 180px;
max-width: 180px;
}
table.changelist-results th:nth-child(3), /* Comment */
table.changelist-results td:nth-child(3) {
width: 200px;
max-width: 200px;
}
table.changelist-results th:nth-child(4), /* User Stats */
table.changelist-results td:nth-child(4) {
width: 120px;
max-width: 120px;
}
table.changelist-results th:nth-child(5), /* Activity */
table.changelist-results td:nth-child(5) {
width: 140px;
max-width: 140px;
}
table.changelist-results th:nth-child(6), /* Status */
table.changelist-results td:nth-child(6) {
width: 160px;
max-width: 160px;
}
/* Ensure text doesn't overflow in server admin */
.changelist-results td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: top;
}
/* Allow wrapping for multi-line server info displays */
.changelist-results td .server-stats,
.changelist-results td .server-activity,
.changelist-results td .server-status {
white-space: normal;
}
/* Server type icons */
.server-type-outline {
color: #3b82f6;
}
.server-type-wireguard {
color: #10b981;
}
/* Tooltip styles for truncated text */
[title] {
cursor: help;
border-bottom: 1px dotted #999;
}
/* Form improvements for move clients page */
.form-row.field-box {
border: 1px solid #ddd;

View File

@@ -18,3 +18,186 @@ function generateRandomString(length) {
return result;
}
// OutlineServer JSON Configuration Functionality
document.addEventListener('DOMContentLoaded', function() {
// JSON Import functionality
const importJsonBtn = document.getElementById('import-json-btn');
const importJsonTextarea = document.getElementById('import-json-config');
if (importJsonBtn && importJsonTextarea) {
// Auto-fill on paste event
importJsonTextarea.addEventListener('paste', function(e) {
// Small delay to let paste complete
setTimeout(() => {
tryAutoFillFromJson();
}, 100);
});
// Manual import button
importJsonBtn.addEventListener('click', function() {
tryAutoFillFromJson();
});
function tryAutoFillFromJson() {
try {
const jsonText = importJsonTextarea.value.trim();
if (!jsonText) {
alert('Please enter JSON configuration');
return;
}
const config = JSON.parse(jsonText);
// Validate required fields
if (!config.apiUrl || !config.certSha256) {
alert('Invalid JSON format. Required fields: apiUrl, certSha256');
return;
}
// Parse apiUrl to extract components
const url = new URL(config.apiUrl);
// Fill form fields
const adminUrlField = document.getElementById('id_admin_url');
const adminCertField = document.getElementById('id_admin_access_cert');
const clientHostnameField = document.getElementById('id_client_hostname');
const clientPortField = document.getElementById('id_client_port');
const nameField = document.getElementById('id_name');
const commentField = document.getElementById('id_comment');
if (adminUrlField) adminUrlField.value = config.apiUrl;
if (adminCertField) adminCertField.value = config.certSha256;
// Use provided hostname or extract from URL
const hostname = config.clientHostname || config.hostnameForAccessKeys || url.hostname;
if (clientHostnameField) clientHostnameField.value = hostname;
// Use provided port or extract from various sources
const clientPort = config.clientPort || config.portForNewAccessKeys || url.port || '1257';
if (clientPortField) clientPortField.value = clientPort;
// Generate server name if not provided and field is empty
if (nameField && !nameField.value) {
const serverName = config.serverName || config.name || `Outline-${hostname}`;
nameField.value = serverName;
}
// Fill comment if provided and field exists
if (commentField && config.comment) {
commentField.value = config.comment;
}
// Clear the JSON input
importJsonTextarea.value = '';
// Show success message
showSuccessMessage('✅ Configuration imported successfully!');
} catch (error) {
alert('Invalid JSON format: ' + error.message);
}
}
}
// Copy to clipboard functionality
window.copyToClipboard = function(elementId) {
const element = document.getElementById(elementId);
if (element) {
const text = element.textContent || element.innerText;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => {
showCopySuccess();
}).catch(err => {
fallbackCopyTextToClipboard(text);
});
} else {
fallbackCopyTextToClipboard(text);
}
}
};
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showCopySuccess();
} catch (err) {
console.error('Failed to copy text: ', err);
}
document.body.removeChild(textArea);
}
function showCopySuccess() {
showSuccessMessage('📋 Copied to clipboard!');
}
function showSuccessMessage(message) {
const alertHtml = `
<div class="alert alert-success alert-dismissible" style="margin: 1rem 0;">
${message}
<button type="button" class="close" aria-label="Close" onclick="this.parentElement.remove()">
<span aria-hidden="true">&times;</span>
</button>
</div>
`;
// Try to find a container for the message
const container = document.querySelector('.card-body') || document.querySelector('#content-main');
if (container) {
container.insertAdjacentHTML('afterbegin', alertHtml);
}
setTimeout(() => {
const alert = document.querySelector('.alert-success');
if (alert) alert.remove();
}, 5000);
}
// Sync server button - handle both static and dynamic buttons
document.addEventListener('click', async function(e) {
if (e.target && (e.target.id === 'sync-server-btn' || e.target.matches('[id="sync-server-btn"]'))) {
const syncBtn = e.target;
const serverId = syncBtn.dataset.serverId;
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
const originalText = syncBtn.textContent;
syncBtn.textContent = '⏳ Syncing...';
syncBtn.disabled = true;
try {
const response = await fetch(`/admin/vpn/outlineserver/${serverId}/sync/`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
showSuccessMessage(`${data.message}`);
setTimeout(() => window.location.reload(), 2000);
} else {
alert('Sync failed: ' + data.error);
}
} catch (error) {
alert('Network error: ' + error.message);
} finally {
syncBtn.textContent = originalText;
syncBtn.disabled = false;
}
}
});
});