mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
feat(dashboard): grants on the Permissions page and on account and character pages
A Grants tab on the Permissions page (with grants_manage: every grant in force, the history, and a form that asks whose it is, searching accounts or characters by name), and a Permission grants card on account and character pages (the account's or character's grants and history; the form with grants_manage). The form picks the kind (dashboard permission, in-game command, every permission of a category, every command up to a GM level) and searches what to grant among only what the signed-in user may grant; grant or deny, an optional expiry and a note. In-force grants have a Remove button when the user may remove them. Players see their own grants, read-only. The Permissions page (and its menu entry) now opens with permissions_manage or grants_manage; the GM level tabs still need permissions_manage. Check: as GM 9, add a grant and a deny from the Permissions page and from an account and a character page, with and without an expiry; remove one; the lists and history update (also in a second tab). As a GM 8 given grants_manage: only the Grants tab shows, and only permissions and commands GM 8 has are offered. As a player: your own account page lists your grants without a form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -470,7 +470,13 @@ void RegisterDashboardRoutes() {
|
||||
SimplePage("/diagnostics", Perm("health_view"), "diagnostics.jinja2", "diagnostics", "Packets, bytes and HTTP requests per second of every server");
|
||||
SimplePage("/players", Perm("players_view"), "players.jinja2", "players", "Who is online, with kick, rescue and teleport");
|
||||
SimplePage("/backups", Perm("backups"), "backups.jinja2", "backups", "Database backups");
|
||||
SimplePage("/permissions", Perm("permissions_manage"), "permissions.jinja2", "permissions", "What each GM level may do");
|
||||
// The GM level tabs need permissions_manage, the grants tab grants_manage
|
||||
Route(eHTTPMethod::GET, "/permissions", 0, "What each GM level may do, and permission grants", [](HTTPReply& reply, const HTTPContext& context) {
|
||||
if (!Can(context, "permissions_manage") && !Can(context, "grants_manage")) {
|
||||
return RenderError(reply, context, eHTTPStatusCode::FORBIDDEN, "You don't have permission to open this page.");
|
||||
}
|
||||
RenderPage(reply, context, "permissions.jinja2", "permissions");
|
||||
});
|
||||
SimplePage("/settings", Perm("settings"), "settings.jinja2", "settings", "Server settings");
|
||||
SimplePage("/client_assets", Perm("client_files"), "client_assets.jinja2", "client_assets", "Browse the game client's files");
|
||||
Route(eHTTPMethod::GET, "/about", 0, "About this server", [](HTTPReply& reply, const HTTPContext& context) {
|
||||
|
||||
175
dDashboardServer/static/js/grants.js
Normal file
175
dDashboardServer/static/js/grants.js
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Permission grants: dashboard permissions and in-game commands given to (or taken from) one account or character.
|
||||
* Grants.mount(el, {type, id}): the grants of one account or character, with a form to add one when the viewer may.
|
||||
* Grants.mount(el, null): every grant in force and the history, with a form that asks whose it is (Permissions page).
|
||||
* The server decides what may be granted (only what the viewer holds, on accounts they may manage); the form offers
|
||||
* only those.
|
||||
*/
|
||||
(function () {
|
||||
var KINDS = [
|
||||
['permission', 'Dashboard permission'],
|
||||
['command', 'In-game command'],
|
||||
['permission_group', 'Every permission of a category'],
|
||||
['command_group', 'Every command up to a GM level']
|
||||
];
|
||||
var catalog = null;
|
||||
|
||||
function loadCatalog() {
|
||||
if (!catalog) {
|
||||
catalog = api.get('/api/grants/catalog').then(function (d) {
|
||||
if (!d.success) throw new Error(d.error || 'Could not load what can be granted');
|
||||
return d;
|
||||
});
|
||||
catalog.catch(function () { catalog = null; });
|
||||
}
|
||||
return catalog;
|
||||
}
|
||||
|
||||
// What can be picked for a kind: [{value, label, detail}], only what the viewer may grant
|
||||
function choices(d, kind, text) {
|
||||
var list;
|
||||
if (kind === 'permission') {
|
||||
list = d.permissions.map(function (p) {
|
||||
return { value: p.key, label: p.title + ' (' + p.key + ')', detail: p.category + ' · GM ' + p.level + '+ · ' + p.description, ok: p.grantable, why: p.reason };
|
||||
});
|
||||
} else if (kind === 'command') {
|
||||
list = d.commands.map(function (c) {
|
||||
return { value: c.name, label: '/' + c.aliases[0] + (c.aliases.length > 1 ? ' (also /' + c.aliases.slice(1).join(', /') + ')' : ''),
|
||||
detail: 'GM ' + c.level + '+' + (c.permission ? ' · follows ' + c.permission : '') + ' · ' + c.help, ok: c.grantable, why: c.reason };
|
||||
});
|
||||
} else if (kind === 'permission_group') {
|
||||
list = d.permissionGroups.map(function (g) {
|
||||
return { value: g.name, label: g.name, detail: g.permissions.join(', '), ok: g.grantable, why: g.reason };
|
||||
});
|
||||
} else {
|
||||
list = d.commandGroups.map(function (g) {
|
||||
return { value: g.name, label: 'Every command up to GM ' + g.name, detail: '', ok: g.grantable, why: g.reason };
|
||||
});
|
||||
}
|
||||
var query = (text || '').toLowerCase();
|
||||
return list.filter(function (item) {
|
||||
return item.ok && (!query || (item.label + ' ' + item.value + ' ' + item.detail).toLowerCase().indexOf(query) !== -1);
|
||||
}).slice(0, 60);
|
||||
}
|
||||
|
||||
function status(g) {
|
||||
if (g.status === 'removed') return fmt.badge('Removed', 'secondary');
|
||||
if (g.status === 'expired') return fmt.badge('Expired', 'secondary');
|
||||
return fmt.badge('In force', 'success');
|
||||
}
|
||||
|
||||
function targetLink(g) {
|
||||
if (g.targetType === 'account') return 'Account ' + fmt.link('/accounts/' + g.targetId, g.targetName || g.targetId);
|
||||
return 'Character ' + fmt.character(g.targetId, g.targetName);
|
||||
}
|
||||
|
||||
function rows(list, options) {
|
||||
if (!list.length) return '<tr><td colspan="6" class="text-body-secondary">' + esc(options.empty) + '</td></tr>';
|
||||
return list.map(function (g) {
|
||||
var when = 'By ' + esc(g.grantedBy || '?') + ', ' + fmt.unix(g.grantedAt) +
|
||||
(g.revokedAt ? '<br>Removed by ' + esc(g.revokedBy) + ', ' + fmt.unix(g.revokedAt) : '');
|
||||
return '<tr><td>' + (g.deny ? fmt.badge('Deny', 'danger') : fmt.badge('Grant', 'primary')) + '</td>' +
|
||||
'<td><div class="fw-semibold">' + esc(g.label) + '</div>' + (options.showTarget ? '<div class="small">' + targetLink(g) + '</div>' : '') +
|
||||
(g.note ? '<div class="small text-body-secondary">' + esc(g.note) + '</div>' : '') + '</td>' +
|
||||
'<td class="small">' + (g.expiresAt ? fmt.unix(g.expiresAt) : 'Never') + '</td>' +
|
||||
'<td class="small">' + when + '</td><td>' + status(g) + '</td>' +
|
||||
'<td class="text-end">' + (g.canRemove ? '<button type="button" class="btn btn-sm btn-outline-danger" data-remove="' + g.id + '">Remove</button>' : '') + '</td></tr>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function table(id, heading) {
|
||||
return (heading ? '<h6 class="mt-3">' + heading + '</h6>' : '') +
|
||||
'<div class="table-responsive"><table class="table table-sm align-middle mb-0"><thead><tr><th></th><th>What</th><th>Expires</th><th>Given</th><th>Status</th><th></th></tr></thead>' +
|
||||
'<tbody data-rows="' + id + '"></tbody></table></div>';
|
||||
}
|
||||
|
||||
function form(chooseTarget) {
|
||||
var target = chooseTarget ? '<div class="col-12 col-md-3"><label class="form-label small mb-1">For</label>' +
|
||||
'<select class="form-select form-select-sm mb-1" data-target-type aria-label="Account or character"><option value="account">Account</option><option value="character">Character</option></select>' +
|
||||
'<input type="search" class="form-control form-control-sm" data-target placeholder="Search by name" aria-label="Who"></div>' : '';
|
||||
return '<form class="row g-2 align-items-end border rounded p-2 mb-3" data-grant-form>' + target +
|
||||
'<div class="col-12 col-md-3"><label class="form-label small mb-1">Kind</label><select class="form-select form-select-sm" data-kind>' +
|
||||
KINDS.map(function (k) { return '<option value="' + k[0] + '">' + esc(k[1]) + '</option>'; }).join('') + '</select></div>' +
|
||||
'<div class="col-12 col-md-' + (chooseTarget ? '3' : '4') + '"><label class="form-label small mb-1">What</label><input type="search" class="form-control form-control-sm" data-name placeholder="Search" aria-label="What to grant"></div>' +
|
||||
'<div class="col-6 col-md-2"><label class="form-label small mb-1">Effect</label><select class="form-select form-select-sm" data-deny><option value="">Grant</option><option value="1">Deny</option></select></div>' +
|
||||
'<div class="col-6 col-md-' + (chooseTarget ? '3' : '3') + '"><label class="form-label small mb-1">Expires (empty: never)</label><input type="datetime-local" class="form-control form-control-sm" data-expires></div>' +
|
||||
'<div class="col-12 col-md-' + (chooseTarget ? '6' : '9') + '"><input type="text" class="form-control form-control-sm" data-note maxlength="255" placeholder="Note (why)" aria-label="Note"></div>' +
|
||||
'<div class="col-12 col-md-3"><button class="btn btn-sm btn-primary w-100" type="submit">Add</button></div>' +
|
||||
'<div class="col-12 small text-body-secondary">Only what you have yourself can be picked. A deny takes it away even when the GM level allows it (never from GM 9).</div></form>';
|
||||
}
|
||||
|
||||
function searchTargets(type, text) {
|
||||
var url = type === 'account' ? '/api/tables/accounts' : '/api/tables/characters';
|
||||
return api.post(url, { search: text, start: 0, length: 10 }).then(function (d) {
|
||||
return (d.data || []).map(function (r) {
|
||||
return { value: String(r.id), label: r.name, detail: type === 'account' ? 'Account ' + r.id + ' · GM ' + (r.gm_level || 0) : 'Character ' + r.id + (r.account_name ? ' · account ' + r.account_name : '') };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.Grants = {
|
||||
mount: function (el, target) {
|
||||
var chooseTarget = !target;
|
||||
el.innerHTML = '<div data-form></div>' + table('active', chooseTarget ? 'In force' : '') +
|
||||
'<details class="mt-3"' + (chooseTarget ? ' open' : '') + '><summary class="small">History (removed and expired ones too)</summary>' + table('history', '') + '</details>';
|
||||
var formHost = el.querySelector('[data-form]');
|
||||
var nameSelect = null, targetSelect = null;
|
||||
|
||||
function load() {
|
||||
var url = chooseTarget ? '/api/grants' : '/api/grants?' + target.type + '=' + encodeURIComponent(target.id);
|
||||
return api.get(url).then(function (d) {
|
||||
if (!d.success) {
|
||||
el.querySelector('[data-rows="active"]').innerHTML = '<tr><td colspan="6" class="text-danger">' + esc(d.error || 'Could not load the grants') + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
var active = chooseTarget ? d.active : d.grants.filter(function (g) { return g.status === 'active'; });
|
||||
var history = chooseTarget ? d.history : d.grants;
|
||||
el.querySelector('[data-rows="active"]').innerHTML = rows(active, { showTarget: chooseTarget, empty: 'Nothing granted or denied.' });
|
||||
el.querySelector('[data-rows="history"]').innerHTML = rows(history, { showTarget: chooseTarget, empty: 'No grants yet.' });
|
||||
if ((chooseTarget || d.canManage) && !formHost.firstChild) setUpForm();
|
||||
});
|
||||
}
|
||||
|
||||
function setUpForm() {
|
||||
formHost.innerHTML = form(chooseTarget);
|
||||
var f = formHost.querySelector('form');
|
||||
var kind = f.querySelector('[data-kind]');
|
||||
nameSelect = SearchSelect(f.querySelector('[data-name]'), {
|
||||
search: function (text) {
|
||||
return loadCatalog().then(function (d) { return choices(d, kind.value, text); }).catch(function (e) { toast(e.message, 'danger'); return []; });
|
||||
}
|
||||
});
|
||||
kind.addEventListener('change', function () { nameSelect.set('', ''); });
|
||||
if (chooseTarget) {
|
||||
var type = f.querySelector('[data-target-type]');
|
||||
targetSelect = SearchSelect(f.querySelector('[data-target]'), { search: function (text) { return searchTargets(type.value, text); } });
|
||||
type.addEventListener('change', function () { targetSelect.set('', ''); });
|
||||
}
|
||||
f.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var who = chooseTarget ? { type: f.querySelector('[data-target-type]').value, id: targetSelect.input.dataset.value } : target;
|
||||
var name = nameSelect.input.dataset.value;
|
||||
if (!who.id) return toast('Pick an account or character', 'warning');
|
||||
if (!name) return toast('Pick what to grant', 'warning');
|
||||
var expires = f.querySelector('[data-expires]').value;
|
||||
var body = { targetType: who.type, target: who.id, kind: kind.value, name: name, deny: f.querySelector('[data-deny]').value === '1',
|
||||
expiresAt: expires ? Math.floor(new Date(expires).getTime() / 1000) : 0, note: f.querySelector('[data-note]').value };
|
||||
api.action('/api/grants', body).then(function (r) {
|
||||
toast(r.message, 'success');
|
||||
nameSelect.set('', '');
|
||||
f.querySelector('[data-note]').value = '';
|
||||
load();
|
||||
}).catch(function () {});
|
||||
});
|
||||
}
|
||||
|
||||
el.addEventListener('click', function (e) {
|
||||
var button = e.target.closest('[data-remove]');
|
||||
if (!button || !confirm('Remove this grant? It stops applying at once.')) return;
|
||||
api.action('/api/grants/' + button.dataset.remove + '/remove', {}).then(function (r) { toast(r.message, 'success'); load(); }).catch(function () {});
|
||||
});
|
||||
if (window.Live) Live.on('grants', Live.throttle(load, 500));
|
||||
load();
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -307,6 +307,16 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_self or can.grants_manage %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header"><h5 class="mb-0">Permission grants</h5></div>
|
||||
<div class="card-body">
|
||||
<p class="small text-body-secondary">Dashboard permissions and in-game commands given to{% if can.grants_manage %} (or taken from){% endif %} this account on top of what its GM level allows. They count on the dashboard and in game, on every character; each character can have its own too (on its page).</p>
|
||||
<div id="grantsPanel" data-account="{{ account.id }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_self or (can.api_keys_manage and can.accounts_view and manage.tools) %}
|
||||
<div class="card mb-4" id="apiKeys" data-account="{{ account.id }}" data-self="{% if is_self %}1{% endif %}" data-can-create="{% if is_self and can.api_access %}1{% endif %}">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
@@ -680,4 +690,11 @@ function disconnectMailAccount() {
|
||||
<script src="/js/strikes.js"></script>
|
||||
<script src="/js/account-links.js"></script>
|
||||
<script src="/js/api-keys.js"></script>
|
||||
<script src="/js/grants.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var panel = document.getElementById('grantsPanel');
|
||||
if (panel) Grants.mount(panel, { type: 'account', id: panel.dataset.account });
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -245,6 +245,16 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if can.grants_manage or character.is_own %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header"><h5 class="mb-0">Permission grants</h5></div>
|
||||
<div class="card-body">
|
||||
<p class="small text-body-secondary">In-game commands and permissions given to{% if can.grants_manage %} (or taken from){% endif %} this character on top of its GM level, while it is logged in. The account's own grants count too.</p>
|
||||
<div id="grantsPanel" data-character="{{ character.id }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card mb-4" id="relatedCard" data-kind="character" data-id="{{ character.id }}"><div class="card-body text-body-secondary">Loading related data…</div></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -760,4 +770,11 @@ function rescueCharacter() {
|
||||
<script src="/js/character-missions.js"></script>
|
||||
<script src="/js/related.js"></script>
|
||||
<script src="/js/mail-view.js"></script>
|
||||
<script src="/js/grants.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var panel = document.getElementById('grantsPanel');
|
||||
if (panel) Grants.mount(panel, { type: 'character', id: panel.dataset.character });
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% set adminPages = ["settings", "settings_history", "permissions", "tasks", "play_keys", "backups", "webhooks", "client_assets", "ugc"] %}
|
||||
{% if can.settings or can.permissions_manage or can.tasks_view or can.play_keys_manage or can.backups or can.webhooks or can.client_files or can.ugc_manage %}
|
||||
{% if can.settings or can.permissions_manage or can.grants_manage or can.tasks_view or can.play_keys_manage or can.backups or can.webhooks or can.client_files or can.ugc_manage %}
|
||||
<div class="nav-group">
|
||||
<button class="nav-group-toggle" type="button" data-bs-toggle="collapse" data-bs-target="#nav-admin" aria-expanded="{% if current_page in adminPages %}true{% else %}false{% endif %}" aria-controls="nav-admin">
|
||||
Server Admin<span class="nav-group-dot" title="Something in here needs attention"></span>
|
||||
@@ -128,7 +128,7 @@
|
||||
<div class="collapse{% if current_page in adminPages %} show{% endif %}" id="nav-admin">
|
||||
<div class="list-group list-group-flush">
|
||||
{% if can.settings %}<a href="/settings" class="list-group-item list-group-item-action{% if current_page == "settings" or current_page == "settings_history" %} active{% endif %}">Settings</a>{% endif %}
|
||||
{% if can.permissions_manage %}<a href="/permissions" class="list-group-item list-group-item-action{% if current_page == "permissions" %} active{% endif %}">Permissions</a>{% endif %}
|
||||
{% if can.permissions_manage or can.grants_manage %}<a href="/permissions" class="list-group-item list-group-item-action{% if current_page == "permissions" %} active{% endif %}">Permissions</a>{% endif %}
|
||||
{% if can.tasks_view %}<a href="/tasks" class="list-group-item list-group-item-action{% if current_page == "tasks" %} active{% endif %}">Scheduled Tasks</a>{% endif %}
|
||||
{% if can.play_keys_manage %}<a href="/play_keys" class="list-group-item list-group-item-action{% if current_page == "play_keys" %} active{% endif %}">Play Keys</a>{% endif %}
|
||||
{% if can.backups %}<a href="/backups" class="list-group-item list-group-item-action{% if current_page == "backups" %} active{% endif %}">Backups</a>{% endif %}
|
||||
|
||||
@@ -23,16 +23,36 @@
|
||||
<div class="mb-3">
|
||||
<h2 class="mb-1">Permissions</h2>
|
||||
<p class="text-body-secondary mb-0">What each GM level may do on the dashboard and in the game. Click a level to make it the lowest one allowed; every level
|
||||
above it is allowed too. Changes apply straight away, no restart needed. GM 9 can always do everything on the dashboard.</p>
|
||||
above it is allowed too. Changes apply straight away, no restart needed. GM 9 can always do everything on the dashboard.
|
||||
<strong>Grants</strong> give (or take away) a permission or command for one account or character.</p>
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs mb-3" id="permTabs" role="tablist">
|
||||
{% if can.permissions_manage %}
|
||||
<li class="nav-item" role="presentation"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabDashboard" data-hash="dashboard" type="button" role="tab">Dashboard <span class="badge rounded-pill text-bg-secondary" id="permCount"></span></button></li>
|
||||
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabCommands" data-hash="commands" type="button" role="tab">In-game commands <span class="badge rounded-pill text-bg-secondary" id="commandCount"></span></button></li>
|
||||
{% endif %}
|
||||
{% if can.grants_manage %}
|
||||
<li class="nav-item" role="presentation"><button class="nav-link{% if not can.permissions_manage %} active{% endif %}" data-bs-toggle="tab" data-bs-target="#tabGrants" data-hash="grants" type="button" role="tab">Grants</button></li>
|
||||
{% endif %}
|
||||
{% if can.permissions_manage %}
|
||||
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabConfig" data-hash="config" type="button" role="tab">Config files</button></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
{% if can.grants_manage %}
|
||||
<div class="tab-pane fade{% if not can.permissions_manage %} show active{% endif %}" id="tabGrants" role="tabpanel">
|
||||
<details class="about-text mb-2"><summary>How grants work</summary><p class="mb-0">A grant gives one account or character a dashboard permission, an
|
||||
in-game command, every permission of a category or every command up to a GM level, on top of what its GM level allows. A deny takes one away
|
||||
even when the GM level allows it (never from GM 9). A deny beats a grant. Account grants count on the dashboard and in game; character grants
|
||||
count in game while that character is logged in. A grant of a permission also covers the commands that follow it. Online players get changes
|
||||
at once. You can only grant what you have yourself, on accounts you may manage; <code>settings</code>, <code>permissions_manage</code>,
|
||||
commands with a fixed level and commands that never go below a GM level (<code>/execute</code>) can't be granted.</p></details>
|
||||
<div class="card"><div class="card-body" id="grantsPanel"></div></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if can.permissions_manage %}
|
||||
<div class="tab-pane fade show active" id="tabDashboard" role="tabpanel">
|
||||
<div class="perm-toolbar d-flex flex-wrap align-items-center gap-2 mb-3">
|
||||
<input type="search" class="form-control form-control-sm" id="permFilter" placeholder="Search permissions" aria-label="Search permissions">
|
||||
@@ -98,12 +118,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/js/grants.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var grantsPanel = document.getElementById('grantsPanel');
|
||||
if (grantsPanel) Grants.mount(grantsPanel, null);
|
||||
|
||||
// The open tab is in the address (#commands, #grants, #config), so it survives a reload and can be linked to
|
||||
var tabs = document.querySelectorAll('#permTabs [data-hash]');
|
||||
Array.prototype.forEach.call(tabs, function (tab) {
|
||||
if (window.location.hash === '#' + tab.dataset.hash) bootstrap.Tab.getOrCreateInstance(tab).show();
|
||||
tab.addEventListener('shown.bs.tab', function () { history.replaceState(null, '', '#' + tab.dataset.hash); });
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
(function () {
|
||||
// The GM level tabs: only with permissions_manage
|
||||
if (!document.getElementById('permRows')) return;
|
||||
var permissions = [], commands = [];
|
||||
var SOURCES = { 'default': ['Default', 'secondary'], web: ['This page', 'primary'], file: ['Config file', 'info'], env: ['Environment', 'info'], fixed: ['Fixed', 'dark'], permission: ['Permission', 'success'] };
|
||||
var filter = document.getElementById('commandFilter'), showClient = document.getElementById('showClient'), commandChanged = document.getElementById('commandChanged');
|
||||
@@ -285,13 +322,6 @@
|
||||
permCategory.addEventListener('change', renderPermissions);
|
||||
permChanged.addEventListener('change', renderPermissions);
|
||||
|
||||
// The open tab is in the address (#commands, #config), so it survives a reload and can be linked to
|
||||
var tabs = document.querySelectorAll('#permTabs [data-hash]');
|
||||
Array.prototype.forEach.call(tabs, function (tab) {
|
||||
if (window.location.hash === '#' + tab.dataset.hash) bootstrap.Tab.getOrCreateInstance(tab).show();
|
||||
tab.addEventListener('shown.bs.tab', function () { history.replaceState(null, '', '#' + tab.dataset.hash); });
|
||||
});
|
||||
|
||||
if (window.Live) Live.on('permissions', Live.throttle(load, 500));
|
||||
load();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user