mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-09-12 19:58:21 +02:00
Security fixes, enterprise stuff and more (#3241)
# Description of Changes Please provide a summary of the changes, including: - Enable user to add custom JAVA ops with env JAVA_CUSTOM_OPTS - Added support for prometheus (enabled via JAVA_CUSTOM_OPTS + enterprise license) - Changed settings from enterprise naming to 'Premium' - KeygenLicense Check to support offline licenses - Disable URL-to-PDF due to huge security bug - Remove loud Split PDF logs - addUsers renamed to adminSettings - Added Usage analytics page - Add user button to only be enabled based on total users free - Improve Merge memory usage Closes #(issue_number) --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: a <a> Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com> Co-authored-by: Connor Yoh <[email protected]>
This commit is contained in:
co-authored by
a
pixeebot[bot]
Connor Yoh
parent
86becc61de
commit
e151286337
@@ -40,4 +40,4 @@ springdoc.api-docs.path=/v1/api-docs
|
||||
# Set the URL of the OpenAPI JSON for the Swagger UI
|
||||
springdoc.swagger-ui.url=/v1/api-docs
|
||||
posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq
|
||||
posthog.host=https://eu.i.posthog.com
|
||||
posthog.host=https://eu.i.posthog.com
|
||||
@@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Active Users:
|
||||
adminUserSettings.disabledUsers=Disabled Users:
|
||||
adminUserSettings.totalUsers=Total Users:
|
||||
adminUserSettings.lastRequest=Last Request
|
||||
adminUserSettings.usage=View Usage
|
||||
|
||||
endpointStatistics.title=Endpoint Statistics
|
||||
endpointStatistics.header=Endpoint Statistics
|
||||
endpointStatistics.top10=Top 10
|
||||
endpointStatistics.top20=Top 20
|
||||
endpointStatistics.all=All
|
||||
endpointStatistics.refresh=Refresh
|
||||
endpointStatistics.includeHomepage=Include Homepage ('/')
|
||||
endpointStatistics.includeLoginPage=Include Login Page ('/login')
|
||||
endpointStatistics.totalEndpoints=Total Endpoints
|
||||
endpointStatistics.totalVisits=Total Visits
|
||||
endpointStatistics.showing=Showing
|
||||
endpointStatistics.selectedVisits=Selected Visits
|
||||
endpointStatistics.endpoint=Endpoint
|
||||
endpointStatistics.visits=Visits
|
||||
endpointStatistics.percentage=Percentage
|
||||
endpointStatistics.loading=Loading...
|
||||
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
|
||||
endpointStatistics.home=Home
|
||||
endpointStatistics.login=Login
|
||||
endpointStatistics.top=Top
|
||||
endpointStatistics.numberOfVisits=Number of Visits
|
||||
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
|
||||
endpointStatistics.retry=Retry
|
||||
|
||||
database.title=Database Import/Export
|
||||
database.header=Database Import/Export
|
||||
|
||||
@@ -61,15 +61,17 @@ security:
|
||||
privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair
|
||||
spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
|
||||
|
||||
enterpriseEdition:
|
||||
enabled: false # set to 'true' to enable enterprise edition
|
||||
|
||||
premium:
|
||||
key: 00000000-0000-0000-0000-000000000000
|
||||
SSOAutoLogin: false # Enable to auto login to first provided SSO
|
||||
CustomMetadata:
|
||||
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values
|
||||
author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username
|
||||
creator: Stirling-PDF # supports text such as 'Company-PDF'
|
||||
producer: Stirling-PDF # supports text such as 'Company-PDF'
|
||||
enabled: false # Enable license key checks for pro/enterprise features
|
||||
proFeatures:
|
||||
SSOAutoLogin: false
|
||||
CustomMetadata:
|
||||
autoUpdateMetadata: false
|
||||
author: username
|
||||
creator: Stirling-PDF
|
||||
producer: Stirling-PDF
|
||||
|
||||
legal:
|
||||
termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder
|
||||
@@ -87,6 +89,7 @@ system:
|
||||
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files
|
||||
tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
|
||||
enableAnalytics: null # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true
|
||||
enableUrlToPDF: false # Set to 'true' to enable URL to PDF, INTERNAL ONLY, known security issues, should not be used externally
|
||||
disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML)
|
||||
datasource:
|
||||
enableCustomDatabase: false # Enterprise users ONLY, set this property to 'true' if you would like to use your own custom database configuration
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
.active-user {
|
||||
color: green;
|
||||
text-shadow: 0 0 5px green;
|
||||
}
|
||||
|
||||
.text-overflow {
|
||||
max-width: 100px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stats-box {
|
||||
background: var(--md-sys-color-outline-variant);
|
||||
padding: .8rem;
|
||||
margin: 10px 0;
|
||||
border-radius: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
margin-top: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.filter-checkbox input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
html[data-bs-theme="light"] .loading {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
html[data-bs-theme="dark"] .loading {
|
||||
background-color: rgba(15, 20, 26, 0.7);
|
||||
}
|
||||
|
||||
/* Add some animation to the refresh button */
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.refreshing .material-symbols-rounded {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
// We'll fetch data from the API instead of hardcoding it
|
||||
let allEndpointData = [];
|
||||
let filteredData = [];
|
||||
|
||||
// We'll store these as global variables that get updated when we fetch data
|
||||
let sortedData = [];
|
||||
let totalEndpoints = 0;
|
||||
let totalVisits = 0;
|
||||
|
||||
// Chart instance
|
||||
let myChart;
|
||||
|
||||
|
||||
// Function to get chart colors based on current theme
|
||||
function getChartColors() {
|
||||
var style = window.getComputedStyle(document.body)
|
||||
|
||||
const colours = {
|
||||
textColor: style.getPropertyValue('--md-sys-color-on-surface') ,
|
||||
primaryColor: style.getPropertyValue('--md-sys-color-primary'),
|
||||
backgroundColor: style.getPropertyValue('--md-sys-color-background'),
|
||||
gridColor: style.getPropertyValue('--md-sys-color-on-surface'),
|
||||
tooltipBgColor: style.getPropertyValue('--md-sys-color-inverse-on-surface'),
|
||||
tooltipTextColor: style.getPropertyValue('--md-sys-color-inverse-surface')
|
||||
}
|
||||
return colours;
|
||||
}
|
||||
|
||||
// Watch for theme changes and update chart if needed
|
||||
function setupThemeChangeListener() {
|
||||
|
||||
// Start observing theme changes
|
||||
document.addEventListener("modeChanged", (event) => {
|
||||
setTimeout(function() {
|
||||
if (myChart) {
|
||||
const currentLimit = document.getElementById('currentlyShowing').textContent;
|
||||
const limit = (currentLimit === endpointStatsTranslations.all)
|
||||
? filteredData.length
|
||||
: (currentLimit === endpointStatsTranslations.top20 ? 20 : 10);
|
||||
updateChart(limit);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Also watch for system preference changes
|
||||
window
|
||||
.matchMedia('(prefers-color-scheme: dark)')
|
||||
.addEventListener('change', () => {
|
||||
if (myChart) {
|
||||
const currentLimit = document.getElementById('currentlyShowing').textContent;
|
||||
const limit = (currentLimit === endpointStatsTranslations.all)
|
||||
? filteredData.length
|
||||
: (currentLimit === endpointStatsTranslations.top20 ? 20 : 10);
|
||||
updateChart(limit);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to filter data based on checkbox settings
|
||||
function filterData() {
|
||||
const includeHome = document.getElementById('hideHomeCheckbox').checked;
|
||||
const includeLogin = document.getElementById('hideLoginCheckbox').checked;
|
||||
|
||||
filteredData = allEndpointData.filter(item => {
|
||||
if (!includeHome && item.endpoint === '/') return false;
|
||||
if (!includeLogin && item.endpoint === '/login') return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Sort and calculate
|
||||
sortedData = [...filteredData].sort((a, b) => b.count - a.count);
|
||||
totalEndpoints = filteredData.length;
|
||||
totalVisits = filteredData.reduce((sum, item) => sum + item.count, 0);
|
||||
|
||||
// Update stats
|
||||
document.getElementById('totalEndpoints').textContent = totalEndpoints.toLocaleString();
|
||||
document.getElementById('totalVisits').textContent = totalVisits.toLocaleString();
|
||||
|
||||
// Update the chart with current limit
|
||||
const currentLimit = document.getElementById('currentlyShowing').textContent;
|
||||
const limit = (currentLimit === endpointStatsTranslations.all)
|
||||
? filteredData.length
|
||||
: (currentLimit === endpointStatsTranslations.top20 ? 20 : 10);
|
||||
updateChart(limit);
|
||||
}
|
||||
|
||||
// Function to fetch data from the API
|
||||
async function fetchEndpointData() {
|
||||
try {
|
||||
// Show loading state
|
||||
const chartContainer = document.querySelector('.chart-container');
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.className = 'loading';
|
||||
loadingDiv.innerHTML = `
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">${endpointStatsTranslations.loading}</span>
|
||||
</div>`;
|
||||
chartContainer.appendChild(loadingDiv);
|
||||
|
||||
// Also add animation to refresh button
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
refreshBtn.classList.add('refreshing');
|
||||
refreshBtn.disabled = true;
|
||||
|
||||
const response = await fetch('/api/v1/info/load/all');
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
allEndpointData = data;
|
||||
|
||||
// Apply filters
|
||||
filterData();
|
||||
|
||||
// Remove loading state
|
||||
chartContainer.removeChild(loadingDiv);
|
||||
refreshBtn.classList.remove('refreshing');
|
||||
refreshBtn.disabled = false;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching endpoint data:', error);
|
||||
// Show error message to user
|
||||
showError(endpointStatsTranslations.failedToLoad);
|
||||
|
||||
// Reset refresh button
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
refreshBtn.classList.remove('refreshing');
|
||||
refreshBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to format endpoint names
|
||||
function formatEndpointName(endpoint) {
|
||||
if (endpoint === '/') return endpointStatsTranslations.home;
|
||||
if (endpoint === '/login') return endpointStatsTranslations.login;
|
||||
return endpoint.replace('/', '').replace(/-/g, ' ');
|
||||
}
|
||||
|
||||
// Function to update the table
|
||||
function updateTable(data) {
|
||||
const tableBody = document.getElementById('endpointTableBody');
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
data.forEach((item, index) => {
|
||||
const percentage = ((item.count / totalVisits) * 100).toFixed(2);
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// Format endpoint for better readability
|
||||
let displayEndpoint = item.endpoint;
|
||||
if (displayEndpoint.length > 40) {
|
||||
displayEndpoint = displayEndpoint.substring(0, 37) + '...';
|
||||
}
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${index + 1}</td>
|
||||
<td title="${item.endpoint}">${displayEndpoint}</td>
|
||||
<td>${item.count.toLocaleString()}</td>
|
||||
<td>${percentage}%</td>
|
||||
`;
|
||||
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to update the chart
|
||||
function updateChart(dataLimit) {
|
||||
const chartData = sortedData.slice(0, dataLimit);
|
||||
|
||||
// Calculate displayed statistics
|
||||
const displayedVisits = chartData.reduce((sum, item) => sum + item.count, 0);
|
||||
const displayedPercentage = totalVisits > 0
|
||||
? ((displayedVisits / totalVisits) * 100).toFixed(2)
|
||||
: '0';
|
||||
|
||||
document.getElementById('displayedVisits').textContent = displayedVisits.toLocaleString();
|
||||
document.getElementById('displayedPercentage').textContent = displayedPercentage;
|
||||
|
||||
// If the limit equals the total filtered items, show "All"; otherwise "Top X"
|
||||
document.getElementById('currentlyShowing').textContent =
|
||||
(dataLimit === filteredData.length)
|
||||
? endpointStatsTranslations.all
|
||||
: endpointStatsTranslations.top + dataLimit;
|
||||
|
||||
// Update the table with new data
|
||||
updateTable(chartData);
|
||||
|
||||
// Prepare labels and datasets
|
||||
const labels = chartData.map(item => formatEndpointName(item.endpoint));
|
||||
const data = chartData.map(item => item.count);
|
||||
|
||||
// Get theme-specific colors
|
||||
const colors = getChartColors();
|
||||
|
||||
// Destroy previous chart if it exists
|
||||
if (myChart) {
|
||||
myChart.destroy();
|
||||
}
|
||||
|
||||
// Create chart context
|
||||
const ctx = document.getElementById('endpointChart').getContext('2d');
|
||||
|
||||
// Create new chart with theme-appropriate colors
|
||||
myChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: endpointStatsTranslations.numberOfVisits,
|
||||
data: data,
|
||||
backgroundColor: colors.primaryColor.replace('rgb', 'rgba').replace(')', ', 0.6)'),
|
||||
borderColor: colors.primaryColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: dataLimit > 20 ? 'x' : 'y',
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top',
|
||||
labels: {
|
||||
color: colors.primaryColor,
|
||||
font: {
|
||||
weight: 'bold'
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: colors.tooltipBgColor,
|
||||
titleColor: colors.tooltipTextColor,
|
||||
bodyColor: colors.tooltipTextColor,
|
||||
borderColor: colors.tooltipBgColor,
|
||||
borderWidth: 1,
|
||||
padding: 12,
|
||||
cornerRadius: 8,
|
||||
titleFont: {
|
||||
size: 14,
|
||||
weight: 'bold'
|
||||
},
|
||||
bodyFont: {
|
||||
size: 13
|
||||
},
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const value = context.raw;
|
||||
const percentage = totalVisits > 0
|
||||
? ((value / totalVisits) * 100).toFixed(2)
|
||||
: '0';
|
||||
// Insert your i18n text in the final string:
|
||||
// e.g. "Visits: 12 (34% of total)"
|
||||
// If your translation includes placeholders, you'd parse them here:
|
||||
return endpointStatsTranslations.visitsTooltip
|
||||
.replace('{0}', value.toLocaleString())
|
||||
.replace('{1}', percentage);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
border: {
|
||||
color: colors.gridColor
|
||||
},
|
||||
ticks: {
|
||||
color: colors.gridColor,
|
||||
font: {
|
||||
size: 12
|
||||
},
|
||||
callback: function(value, index, values) {
|
||||
let label = this.getLabelForValue(value);
|
||||
return label.length > 15 ? label.substr(0, 15) + '...' : label;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: `${colors.gridColor}`
|
||||
}
|
||||
},
|
||||
y: {
|
||||
border: {
|
||||
color: colors.gridColor
|
||||
},
|
||||
min: 0,
|
||||
ticks: {
|
||||
color: colors.gridColor,
|
||||
font: {
|
||||
size: 12
|
||||
},
|
||||
precision: 0
|
||||
},
|
||||
grid: {
|
||||
color: `${colors.gridColor}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize with fetch and top 10
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Set up theme change listener
|
||||
setupThemeChangeListener();
|
||||
|
||||
// Initial data fetch
|
||||
fetchEndpointData();
|
||||
|
||||
// Set up button event listeners
|
||||
document.getElementById('top10Btn').addEventListener('click', function() {
|
||||
updateChart(10);
|
||||
setActiveButton(this);
|
||||
});
|
||||
|
||||
document.getElementById('top20Btn').addEventListener('click', function() {
|
||||
updateChart(20);
|
||||
setActiveButton(this);
|
||||
});
|
||||
|
||||
document.getElementById('allBtn').addEventListener('click', function() {
|
||||
updateChart(filteredData.length);
|
||||
setActiveButton(this);
|
||||
});
|
||||
|
||||
document.getElementById('refreshBtn').addEventListener('click', function() {
|
||||
fetchEndpointData();
|
||||
});
|
||||
|
||||
// Set up filter checkbox listeners
|
||||
document.getElementById('hideHomeCheckbox').addEventListener('change', filterData);
|
||||
document.getElementById('hideLoginCheckbox').addEventListener('change', filterData);
|
||||
});
|
||||
|
||||
function setActiveButton(activeButton) {
|
||||
// Remove active class from all buttons
|
||||
document.querySelectorAll('.chart-controls button').forEach(button => {
|
||||
button.classList.remove('active');
|
||||
});
|
||||
// Add active class to clicked button
|
||||
activeButton.classList.add('active');
|
||||
}
|
||||
|
||||
// Function to handle errors in a user-friendly way
|
||||
function showError(message) {
|
||||
const chartContainer = document.querySelector('.chart-container');
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'alert alert-danger';
|
||||
errorDiv.innerHTML = `
|
||||
<span class="material-symbols-rounded" style="vertical-align: bottom; margin-right: 5px;">error</span>
|
||||
${message}
|
||||
<button id="errorRetryBtn" class="btn btn-outline-danger btn-sm" style="margin-left: 10px;">
|
||||
<span class="material-symbols-rounded" style="font-size: 1rem; vertical-align: bottom;">refresh</span>
|
||||
${endpointStatsTranslations.retry}
|
||||
</button>
|
||||
`;
|
||||
|
||||
chartContainer.innerHTML = '';
|
||||
chartContainer.appendChild(errorDiv);
|
||||
|
||||
// Add retry button functionality
|
||||
document.getElementById('errorRetryBtn').addEventListener('click', fetchEndpointData);
|
||||
}
|
||||
@@ -349,7 +349,7 @@
|
||||
</script>
|
||||
<div class="mb-3 mt-4 text-center">
|
||||
<a th:href="@{'/logout'}" role="button" class="btn btn-danger" th:text="#{account.signOut}">Sign Out</a>
|
||||
<a th:if="${role == 'ROLE_ADMIN'}" class="btn btn-info" th:href="@{'/addUsers'}" role="button" th:text="#{account.adminSettings}" target="_blank">Admin Settings</a>
|
||||
<a th:if="${role == 'ROLE_ADMIN'}" class="btn btn-info" th:href="@{'/adminSettings'}" role="button" th:text="#{account.adminSettings}" target="_blank">Admin Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+41
-16
@@ -32,21 +32,46 @@
|
||||
</div>
|
||||
|
||||
<!-- User Settings Title -->
|
||||
<div style="background: var(--md-sys-color-outline-variant);padding: .8rem; margin: 10px 0; border-radius: 2rem; text-align: center;">
|
||||
<a href="#" data-bs-toggle="modal" data-bs-target="#addUserModal" class="btn btn-outline-success" th:title="#{adminUserSettings.addUser}">
|
||||
<span class="material-symbols-rounded">person_add</span>
|
||||
<span th:text="#{adminUserSettings.addUser}">Add New User</span>
|
||||
</a>
|
||||
<a href="#" data-bs-toggle="modal" data-bs-target="#changeUserRoleModal" class="btn btn-outline-success" th:title="#{adminUserSettings.changeUserRole}">
|
||||
<span class="material-symbols-rounded">edit</span>
|
||||
<span th:text="#{adminUserSettings.changeUserRole}">Change User's Role</span>
|
||||
</a>
|
||||
<div class="my-4">
|
||||
<strong th:text="#{adminUserSettings.totalUsers}">Total Users:</strong> <span th:text="${totalUsers}"></span>
|
||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.activeUsers}">Active Users:</strong> <span th:text="${activeUsers}"></span>
|
||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.disabledUsers}">Disabled Users:</strong> <span th:text="${disabledUsers}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background: var(--md-sys-color-outline-variant);padding: .8rem; margin: 10px 0; border-radius: 2rem; text-align: center;">
|
||||
<a href="#"
|
||||
th:data-bs-toggle="${@runningEE && totalUsers >= maxEnterpriseUsers} ? null : 'modal'"
|
||||
th:data-bs-target="${@runningEE && totalUsers >= maxEnterpriseUsers} ? null : '#addUserModal'"
|
||||
th:class="${@runningEE && totalUsers >= maxEnterpriseUsers} ? 'btn btn-danger' : 'btn btn-outline-success'"
|
||||
th:title="${@runningEE && totalUsers >= maxEnterpriseUsers} ? #{adminUserSettings.maxUsersReached} : #{adminUserSettings.addUser}">
|
||||
<span class="material-symbols-rounded">person_add</span>
|
||||
<span th:text="#{adminUserSettings.addUser}">Add New User</span>
|
||||
</a>
|
||||
|
||||
<a href="#"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#changeUserRoleModal"
|
||||
class="btn btn-outline-success"
|
||||
th:title="#{adminUserSettings.changeUserRole}">
|
||||
<span class="material-symbols-rounded">edit</span>
|
||||
<span th:text="#{adminUserSettings.changeUserRole}">Change User's Role</span>
|
||||
</a>
|
||||
|
||||
<a href="/usage"
|
||||
class="btn btn-outline-success"
|
||||
th:title="#{adminUserSettings.usage}">
|
||||
<span class="material-symbols-rounded">analytics</span>
|
||||
<span th:text="#{adminUserSettings.usage}">Usage Statistics</span>
|
||||
</a>
|
||||
|
||||
<div class="my-4">
|
||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalUsers}">Total Users:</strong>
|
||||
<span th:text="${totalUsers}"></span>
|
||||
<span th:if="${@runningEE}" th:text="'/'+${maxEnterpriseUsers}"></span>
|
||||
|
||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.activeUsers}">Active Users:</strong>
|
||||
<span th:text="${activeUsers}"></span>
|
||||
|
||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.disabledUsers}">Disabled Users:</strong>
|
||||
<span th:text="${disabledUsers}"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div th:if="${addMessage}" class="p-3" style="background: var(--md-sys-color-outline-variant);border-radius: 2rem; text-align: center;">
|
||||
<div class="alert alert-danger mb-auto">
|
||||
<span th:text="#{${addMessage}}">Default message if not found</span>
|
||||
@@ -101,7 +126,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p th:text="#{enterpriseEdition.ssoAdvert}"></p>
|
||||
<p th:if="${!@runningEE}" th:text="#{enterpriseEdition.ssoAdvert}"></p>
|
||||
|
||||
<script th:inline="javascript">
|
||||
const delete_confirm_text = /*[[#{adminUserSettings.confirmDeleteUser}]]*/ 'Should the user be deleted?';
|
||||
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{endpointStatistics.title}, header=#{endpointStatistics.header})}"></th:block>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
|
||||
<link rel="stylesheet" th:href="@{'/css/usage.css'}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-9 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon">analytics</span>
|
||||
<span class="tool-header-text" th:text="#{endpointStatistics.header}">Endpoint Statistics</span>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Summary Box -->
|
||||
<div class="stats-box">
|
||||
<div class="chart-controls">
|
||||
<button id="top10Btn" class="btn btn-outline-primary active">
|
||||
<span class="material-symbols-rounded">bar_chart</span>
|
||||
<span th:text="#{endpointStatistics.top10}">Top 10</span>
|
||||
</button>
|
||||
<button id="top20Btn" class="btn btn-outline-primary">
|
||||
<span class="material-symbols-rounded">data_usage</span>
|
||||
<span th:text="#{endpointStatistics.top20}">Top 20</span>
|
||||
</button>
|
||||
<button id="allBtn" class="btn btn-outline-primary">
|
||||
<span class="material-symbols-rounded">insights</span>
|
||||
<span th:text="#{endpointStatistics.all}">All</span>
|
||||
</button>
|
||||
<button id="refreshBtn" class="btn btn-outline-secondary">
|
||||
<span class="material-symbols-rounded">refresh</span>
|
||||
<span th:text="#{endpointStatistics.refresh}">Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-controls">
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="hideHomeCheckbox" checked>
|
||||
<span th:text="#{endpointStatistics.includeHomepage}">Include Homepage ('/')</span>
|
||||
</label>
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="hideLoginCheckbox" checked>
|
||||
<span th:text="#{endpointStatistics.includeLoginPage}">Include Login Page ('/login')</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="my-4" style="color: var(--md-sys-color-on-surface); font-weight: 500;">
|
||||
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.totalEndpoints} + ':'">Total Endpoints:</strong> <span id="totalEndpoints">0</span></span>
|
||||
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.totalVisits} + ':'">Total Visits:</strong> <span id="totalVisits">0</span></span>
|
||||
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.showing} + ':'">Showing:</strong> <span id="currentlyShowing">Top 10</span></span>
|
||||
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.selectedVisits} + ':'">Selected Visits:</strong> <span id="displayedVisits">0</span> (<span id="displayedPercentage">0</span>%)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart Container -->
|
||||
<div class="bg-card mt-3 mb-3">
|
||||
<div class="chart-container">
|
||||
<canvas id="endpointChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table for detailed data -->
|
||||
<div class="bg-card mt-3 mb-3">
|
||||
<table class="endpoint-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 5%;">#</th>
|
||||
<th scope="col" style="width: 55%;" th:text="#{endpointStatistics.endpoint}">Endpoint</th>
|
||||
<th scope="col" style="width: 20%;" th:text="#{endpointStatistics.visits}">Visits</th>
|
||||
<th scope="col" style="width: 20%;" th:text="#{endpointStatistics.percentage}">Percentage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="endpointTableBody">
|
||||
<!-- Table rows will be dynamically generated -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" th:src="@{'/js/usage.js'}"></script>
|
||||
<script th:inline="javascript">
|
||||
const endpointStatsTranslations = {
|
||||
all: /*[[#{endpointStatistics.all}]]*/ 'All',
|
||||
top20: /*[[#{endpointStatistics.top20}]]*/ 'Top 20',
|
||||
loading: /*[[#{endpointStatistics.loading}]]*/ 'Loading...',
|
||||
failedToLoad: /*[[#{endpointStatistics.failedToLoad}]]*/ 'Failed to load endpoint data. Please try refreshing.',
|
||||
home: /*[[#{endpointStatistics.home}]]*/ 'Home',
|
||||
login: /*[[#{endpointStatistics.login}]]*/ 'Login',
|
||||
top: /*[[#{endpointStatistics.top}]]*/ 'Top ',
|
||||
numberOfVisits: /*[[#{endpointStatistics.numberOfVisits}]]*/ 'Number of Visits',
|
||||
visitsTooltip: /*[[#{endpointStatistics.visitsTooltip}]]*/ 'Visits: {0} ({1}% of total)',
|
||||
retry: /*[[#{endpointStatistics.retry}]]*/ 'Retry'
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user