mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-05-21 11:40:38 +02:00
Plugins: fix installed/active counts, 404s, metadata sync, install fallback
- pluginHolder/views: use dir+meta.xml for installed count; exclude core apps; repair pass to restore meta.xml from source or GitHub; ensure_plugin_meta_xml falls back to GitHub when source missing; cap active <= installed - pluginHolder/urls: include plugin routes for all on-disk plugins (not only INSTALLED_APPS) so /plugins/<name>/settings/ works after install - pluginHolder/plugins.html: Install button tries local then store (GitHub) - CyberCP/settings: sync INSTALLED_APPS with plugin dirs on disk (meta.xml+urls.py) Author: master3395
This commit is contained in:
@@ -430,13 +430,20 @@
|
||||
.installed-sort-filter-bar {
|
||||
flex-basis: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid var(--border-primary, #e2e8f0);
|
||||
}
|
||||
.installed-sort-filter-bar .installed-filter-row,
|
||||
.installed-sort-filter-bar .installed-sort-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.installed-sort-filter-bar .sort-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
@@ -1224,10 +1231,25 @@
|
||||
{% trans "Plugin Development Guide" %}
|
||||
</a>
|
||||
</div>
|
||||
<!-- Sort bar for Grid and Table view (visible when grid or table is active) -->
|
||||
<!-- Filter + Sort bar for Grid and Table view (visible when grid or table is active) -->
|
||||
<div id="installedSortFilterBar" class="installed-sort-filter-bar" style="display: none;"
|
||||
data-trans-name-asc="{% trans 'Name A-Å' %}" data-trans-name-desc="{% trans 'Name Å-A' %}"
|
||||
data-trans-date-newest="{% trans 'Date (newest)' %}" data-trans-date-oldest="{% trans 'Date (oldest)' %}">
|
||||
data-trans-date-newest="{% trans 'Date (newest)' %}" data-trans-date-oldest="{% trans 'Date (oldest)' }}">
|
||||
<div class="installed-filter-row">
|
||||
<span class="sort-label">{% trans "Show:" %}</span>
|
||||
<div class="filter-btns sort-btns">
|
||||
<button type="button" class="sort-btn filter-btn active" data-filter="all" id="installedFilterBtnAll" onclick="setInstalledFilter('all')" title="{% trans 'Show all plugins' %}">
|
||||
<i class="fas fa-th-list"></i> <span class="filter-btn-label">{% trans "All" %}</span>
|
||||
</button>
|
||||
<button type="button" class="sort-btn filter-btn" data-filter="installed" id="installedFilterBtnInstalled" onclick="setInstalledFilter('installed')" title="{% trans 'Show only installed plugins' %}">
|
||||
<i class="fas fa-check-circle"></i> <span class="filter-btn-label">{% trans "Installed only" %}</span>
|
||||
</button>
|
||||
<button type="button" class="sort-btn filter-btn" data-filter="active" id="installedFilterBtnActive" onclick="setInstalledFilter('active')" title="{% trans 'Show only active (enabled) plugins' %}">
|
||||
<i class="fas fa-power-off"></i> <span class="filter-btn-label">{% trans "Active only" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="installed-sort-row">
|
||||
<span class="sort-label">{% trans "Sort by:" %}</span>
|
||||
<div class="sort-btns">
|
||||
<button type="button" class="sort-btn active" data-sort-field="name" id="installedSortBtnName" onclick="toggleInstalledSort('name')" title="{% trans 'Click to toggle A-Å / Å-A' %}">
|
||||
@@ -1240,13 +1262,14 @@
|
||||
<i class="fas fa-calendar-alt"></i> <span class="sort-btn-label">{% trans "Date (newest)" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid View -->
|
||||
<div id="gridView" class="plugins-grid">
|
||||
{% for plugin in plugins %}
|
||||
<div class="plugin-card" data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}">
|
||||
<div class="plugin-card" data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}" data-installed="{{ plugin.installed|yesno:'true,false' }}" data-enabled="{{ plugin.enabled|yesno:'true,false' }}">
|
||||
<div class="plugin-header">
|
||||
<div class="plugin-icon">
|
||||
{% if plugin.type|lower == "security" %}
|
||||
@@ -1388,7 +1411,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for plugin in plugins %}
|
||||
<tr data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}">
|
||||
<tr data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}" data-installed="{{ plugin.installed|yesno:'true,false' }}" data-enabled="{{ plugin.enabled|yesno:'true,false' }}">
|
||||
<td>
|
||||
<strong>{{ plugin.name }}</strong>
|
||||
{% if plugin.freshness_badge %}
|
||||
@@ -1658,6 +1681,7 @@ let currentCategory = 'all';
|
||||
let currentSearchQuery = '';
|
||||
let isSettingHash = false; // Flag to prevent infinite loops
|
||||
let currentInstalledSort = 'name-asc'; // name-asc, name-desc, type, date-desc, date-asc
|
||||
let currentInstalledFilter = 'all'; // all, installed, active
|
||||
|
||||
// Get CSRF cookie helper function
|
||||
function getCookie(name) {
|
||||
@@ -2035,14 +2059,37 @@ function clearPluginSearch() {
|
||||
}
|
||||
}
|
||||
|
||||
function setInstalledFilter(filter) {
|
||||
currentInstalledFilter = filter;
|
||||
var bar = document.getElementById('installedSortFilterBar');
|
||||
if (bar) {
|
||||
try {
|
||||
bar.querySelectorAll('.filter-btn').forEach(function(btn) {
|
||||
btn.classList.toggle('active', (btn.getAttribute('data-filter') || '') === filter);
|
||||
});
|
||||
} catch (e) { console.warn('setInstalledFilter: filter buttons', e); }
|
||||
}
|
||||
try {
|
||||
filterInstalledPlugins();
|
||||
} catch (e) { console.warn('setInstalledFilter: filterInstalledPlugins', e); }
|
||||
}
|
||||
|
||||
function filterInstalledPlugins() {
|
||||
const query = (document.getElementById('installedPluginSearchInput') && document.getElementById('installedPluginSearchInput').value) || '';
|
||||
const terms = query.trim().toLowerCase().split(/\s+/).filter(function(t) { return t.length > 0; });
|
||||
const filter = currentInstalledFilter || 'all';
|
||||
const gridView = document.getElementById('gridView');
|
||||
const tableView = document.getElementById('tableView');
|
||||
const noResultsGrid = document.getElementById('installedPluginsNoResultsGrid');
|
||||
const noResultsTable = document.getElementById('installedPluginsNoResultsTable');
|
||||
if (!gridView && !tableView) return;
|
||||
var visibleCount = 0;
|
||||
function matchesFilter(installed, enabled) {
|
||||
if (filter === 'all') return true;
|
||||
if (filter === 'installed') return installed === 'true';
|
||||
if (filter === 'active') return installed === 'true' && enabled === 'true';
|
||||
return true;
|
||||
}
|
||||
if (gridView) {
|
||||
var cards = gridView.querySelectorAll('.plugin-card');
|
||||
cards.forEach(function(card) {
|
||||
@@ -2050,7 +2097,11 @@ function filterInstalledPlugins() {
|
||||
var desc = (card.getAttribute('data-plugin-desc') || '').toLowerCase();
|
||||
var type = (card.getAttribute('data-plugin-type') || '').toLowerCase();
|
||||
var combined = name + ' ' + desc + ' ' + type;
|
||||
var show = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; });
|
||||
var searchMatch = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; });
|
||||
var installed = card.getAttribute('data-installed') || 'false';
|
||||
var enabled = card.getAttribute('data-enabled') || 'false';
|
||||
var filterMatch = matchesFilter(installed, enabled);
|
||||
var show = searchMatch && filterMatch;
|
||||
card.style.display = show ? '' : 'none';
|
||||
if (show) visibleCount++;
|
||||
});
|
||||
@@ -2065,17 +2116,22 @@ function filterInstalledPlugins() {
|
||||
var desc = (row.getAttribute('data-plugin-desc') || '').toLowerCase();
|
||||
var type = (row.getAttribute('data-plugin-type') || '').toLowerCase();
|
||||
var combined = name + ' ' + desc + ' ' + type;
|
||||
var show = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; });
|
||||
var searchMatch = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; });
|
||||
var installed = row.getAttribute('data-installed') || 'false';
|
||||
var enabled = row.getAttribute('data-enabled') || 'false';
|
||||
var filterMatch = matchesFilter(installed, enabled);
|
||||
var show = searchMatch && filterMatch;
|
||||
row.style.display = show ? '' : 'none';
|
||||
if (show) visibleCount++;
|
||||
});
|
||||
}
|
||||
}
|
||||
var hasFilterOrSearch = terms.length > 0 || (filter !== 'all');
|
||||
if (noResultsGrid) {
|
||||
noResultsGrid.style.display = (terms.length > 0 && visibleCount === 0) ? 'block' : 'none';
|
||||
noResultsGrid.style.display = (hasFilterOrSearch && visibleCount === 0) ? 'block' : 'none';
|
||||
}
|
||||
if (noResultsTable) {
|
||||
noResultsTable.style.display = (terms.length > 0 && visibleCount === 0) ? 'table-row' : 'none';
|
||||
noResultsTable.style.display = (hasFilterOrSearch && visibleCount === 0) ? 'table-row' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2386,43 +2442,54 @@ function installPlugin(pluginName) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Installing...';
|
||||
|
||||
fetch(`/plugins/api/install/${pluginName}/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'Installation Failed!',
|
||||
text: data.error || 'Failed to install plugin',
|
||||
type: 'error'
|
||||
});
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'Failed to install plugin'));
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
const headers = {
|
||||
'X-CSRFToken': getCookie('csrftoken'),
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
function showError(msg) {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'Error!',
|
||||
text: 'Failed to install plugin: ' + error.message,
|
||||
type: 'error'
|
||||
});
|
||||
new PNotify({ title: 'Installation Failed!', text: msg || 'Failed to install plugin', type: 'error' });
|
||||
} else {
|
||||
alert('Error: Failed to install plugin - ' + error.message);
|
||||
alert('Error: ' + (msg || 'Failed to install plugin'));
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
}
|
||||
|
||||
function tryLocalInstall() {
|
||||
return fetch(`/plugins/api/install/${pluginName}/`, { method: 'POST', headers: headers })
|
||||
.then(function(r) { return r.json().then(function(data) { return { response: r, data: data }; }); });
|
||||
}
|
||||
|
||||
function tryStoreInstall() {
|
||||
return fetch(`/plugins/api/store/install/${pluginName}/`, { method: 'POST', headers: headers })
|
||||
.then(function(r) { return r.json().then(function(data) { return { response: r, data: data }; }); });
|
||||
}
|
||||
|
||||
tryLocalInstall()
|
||||
.then(function(result) {
|
||||
if (result.data.success) {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
var err = result.data.error || '';
|
||||
if (result.response.status === 404 || (err && err.indexOf('Plugin source not found') !== -1)) {
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Installing from store...';
|
||||
return tryStoreInstall();
|
||||
}
|
||||
showError(err);
|
||||
})
|
||||
.then(function(result) {
|
||||
if (!result || !result.data) return;
|
||||
if (result.data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
showError(result.data.error || 'Failed to install plugin');
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
showError(error && error.message ? error.message : 'Failed to install plugin');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2978,7 +3045,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Set initial view without updating hash (only update hash if there was already one)
|
||||
const hadHash = hash.length > 0;
|
||||
toggleView(initialView, hadHash);
|
||||
try {
|
||||
toggleView(initialView, hadHash);
|
||||
} catch (e) {
|
||||
console.warn('plugins: toggleView on load failed', e);
|
||||
if (storeView) storeView.style.display = 'block';
|
||||
if (gridView) gridView.style.display = 'none';
|
||||
if (tableView) tableView.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
// Elements don't exist (no plugins installed), just show store view directly
|
||||
if (storeView) {
|
||||
|
||||
Reference in New Issue
Block a user