Various improvements for usability and consistency and inclusion of PWA handling.
This commit is contained in:
@@ -7,47 +7,103 @@ let selectedRole = '';
|
||||
let selectedApi = -1;
|
||||
let currentFilePath = '/';
|
||||
let filesLoaded = false;
|
||||
let busyCount = 0;
|
||||
let uiBusyCount = 0;
|
||||
let serverBusyCount = 0;
|
||||
let currentUserRoles = [];
|
||||
|
||||
const el = id => document.getElementById(id);
|
||||
const hasCurrentRole = role => currentUserRoles.includes(role);
|
||||
|
||||
function setGlobalBusy(on) {
|
||||
busyCount += on ? 1 : -1;
|
||||
if (busyCount < 0) busyCount = 0;
|
||||
el('busyChip').classList.toggle('hidden', busyCount == 0);
|
||||
function setProjectName(projectName) {
|
||||
if (!projectName) return;
|
||||
document.title = projectName;
|
||||
el('appTitle').textContent = projectName;
|
||||
}
|
||||
|
||||
async function tracked(fn) {
|
||||
setGlobalBusy(true);
|
||||
async function loadAppInfo() {
|
||||
try {
|
||||
const response = await fetch('/api/app-info');
|
||||
const json = await response.json();
|
||||
setProjectName(json.projectName || '');
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
function setActivityBusy(kind, on) {
|
||||
if (kind == 'server') {
|
||||
serverBusyCount += on ? 1 : -1;
|
||||
if (serverBusyCount < 0) serverBusyCount = 0;
|
||||
el('serverBusyChip').classList.toggle('hidden', serverBusyCount == 0);
|
||||
return;
|
||||
}
|
||||
uiBusyCount += on ? 1 : -1;
|
||||
if (uiBusyCount < 0) uiBusyCount = 0;
|
||||
el('uiBusyChip').classList.toggle('hidden', uiBusyCount == 0);
|
||||
}
|
||||
|
||||
async function tracked(fn, kind = 'ui') {
|
||||
setActivityBusy(kind, true);
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
setGlobalBusy(false);
|
||||
setActivityBusy(kind, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function apiFetch(url, options) {
|
||||
return await tracked(() => fetch(url, options));
|
||||
async function apiFetch(url, options, activity = 'ui') {
|
||||
return await tracked(() => fetch(url, options), activity);
|
||||
}
|
||||
|
||||
async function req(method, url, body) {
|
||||
async function req(method, url, body, activity = 'ui') {
|
||||
const options = {method, headers: {'Content-Type': 'application/json'}};
|
||||
if (token) options.headers.Authorization = 'Bearer ' + token;
|
||||
if (body !== undefined && method != 'GET') options.body = JSON.stringify(body);
|
||||
const response = await apiFetch(url, options);
|
||||
const response = await apiFetch(url, options, activity);
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
async function serverReq(method, url, body) {
|
||||
return await req(method, url, body, 'server');
|
||||
}
|
||||
|
||||
window.App = {
|
||||
apiFetch,
|
||||
req,
|
||||
token: () => token
|
||||
serverReq,
|
||||
setActivityBusy,
|
||||
token: () => token,
|
||||
roles: () => [...currentUserRoles]
|
||||
};
|
||||
|
||||
const topLevelTabs = [
|
||||
{id: 'dashboard', role: 'WebUIConnect'},
|
||||
{id: 'access', role: 'AccessAdmin'},
|
||||
{id: 'network', role: 'NetworkAdmin'},
|
||||
{id: 'ops', role: 'SystemAdmin'}
|
||||
];
|
||||
|
||||
function visibleTopLevelTabs() {
|
||||
return topLevelTabs.filter(tab => hasCurrentRole(tab.role)).map(tab => tab.id);
|
||||
}
|
||||
|
||||
function applyRoleVisibility() {
|
||||
const visible = visibleTopLevelTabs();
|
||||
topLevelTabs.forEach(tab => {
|
||||
const allowed = visible.includes(tab.id);
|
||||
document.querySelectorAll('[data-tab="' + tab.id + '"]').forEach(button => button.classList.toggle('hidden', !allowed));
|
||||
const panel = el(tab.id);
|
||||
if (panel) panel.classList.toggle('hidden', !allowed);
|
||||
});
|
||||
const topTabs = document.querySelector('#app > .tabs');
|
||||
if (topTabs) topTabs.classList.toggle('hidden', visible.length <= 1);
|
||||
if (visible.length) showTab(visible[0]);
|
||||
return visible;
|
||||
}
|
||||
|
||||
function showTab(id) {
|
||||
if (!visibleTopLevelTabs().includes(id)) return;
|
||||
document.querySelectorAll('.tab[data-tab]').forEach(button => button.classList.toggle('active', button.dataset.tab == id));
|
||||
document.querySelectorAll('.panel').forEach(panel => panel.classList.toggle('active', panel.id == id));
|
||||
if (id == 'network') loadSettings();
|
||||
if (id == 'network') loadNetworkSettings();
|
||||
if (id == 'ops' && !filesLoaded) loadFiles('/');
|
||||
}
|
||||
|
||||
@@ -65,12 +121,40 @@ async function doLogin() {
|
||||
const json = JSON.parse(text);
|
||||
if (!json.success) return;
|
||||
token = json.token;
|
||||
currentUserRoles = (json.roles || '').split('|').filter(Boolean);
|
||||
el('loginPanel').classList.add('hidden');
|
||||
el('app').classList.remove('hidden');
|
||||
window.dispatchEvent(new CustomEvent('app:login', {detail: {token}}));
|
||||
loadSettings();
|
||||
loadAccess();
|
||||
loadFiles('/');
|
||||
el('logoutBtn').classList.remove('hidden');
|
||||
window.dispatchEvent(new CustomEvent('app:login', {detail: {token, roles: currentUserRoles}}));
|
||||
const visible = applyRoleVisibility();
|
||||
if (hasCurrentRole('SystemAdmin')) loadSettings();
|
||||
if (hasCurrentRole('AccessAdmin')) loadAccess();
|
||||
if (hasCurrentRole('NetworkAdmin')) loadNetworkSettings();
|
||||
if (visible.includes('ops')) loadFiles('/');
|
||||
}
|
||||
|
||||
async function doLogout() {
|
||||
if (token) {
|
||||
try {
|
||||
await req('POST', '/api/logout');
|
||||
} catch (error) {}
|
||||
}
|
||||
token = '';
|
||||
currentUserRoles = [];
|
||||
roles = [];
|
||||
users = [];
|
||||
apis = [];
|
||||
selectedUser = '';
|
||||
selectedRole = '';
|
||||
selectedApi = -1;
|
||||
filesLoaded = false;
|
||||
currentFilePath = '/';
|
||||
el('app').classList.add('hidden');
|
||||
el('loginPanel').classList.remove('hidden');
|
||||
el('logoutBtn').classList.add('hidden');
|
||||
el('pass').value = '';
|
||||
el('loginOut').textContent = '';
|
||||
window.dispatchEvent(new CustomEvent('app:logout'));
|
||||
}
|
||||
|
||||
function esc(value) {
|
||||
@@ -443,6 +527,16 @@ async function factoryResetDevice() {
|
||||
el('maintOut').textContent = await req('POST', '/api/factory-reset');
|
||||
}
|
||||
|
||||
async function restartDevice() {
|
||||
if (!confirm('Restart this device now?')) return;
|
||||
setBusy('restartDevice', true, 'Restarting');
|
||||
try {
|
||||
el('maintOut').textContent = await req('POST', '/api/restart');
|
||||
} finally {
|
||||
setBusy('restartDevice', false, 'Restart');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
const text = await req('GET', '/api/logs');
|
||||
try {
|
||||
@@ -472,12 +566,20 @@ async function loadSettings() {
|
||||
const json = JSON.parse(text);
|
||||
if (!json.success) return;
|
||||
const settings = json.settings;
|
||||
const projectName = settings.projectName || 'ESP32-C3';
|
||||
el('appTitle').textContent = projectName + ' Admin';
|
||||
document.title = projectName + ' Admin';
|
||||
setProjectName(settings.projectName);
|
||||
el('logLevel').value = settings.logLevel;
|
||||
el('logMax').value = settings.maxLogBytes;
|
||||
el('upd').value = settings.updateUrl;
|
||||
window.dispatchEvent(new CustomEvent('app:settings-loaded', {detail: {settings}}));
|
||||
}
|
||||
|
||||
async function loadNetworkSettings() {
|
||||
const text = await req('GET', '/api/network');
|
||||
el('networkOut').textContent = text;
|
||||
const json = JSON.parse(text);
|
||||
if (!json.success) return;
|
||||
const settings = json.settings;
|
||||
setProjectName(settings.projectName);
|
||||
el('netHost').value = settings.hostname || '';
|
||||
el('netHost').placeholder = settings.defaultHostname || 'Default host name';
|
||||
el('netDhcp').value = String(settings.dhcp !== false);
|
||||
@@ -488,7 +590,6 @@ async function loadSettings() {
|
||||
el('netDns2').value = settings.dns2 || '';
|
||||
el('netCurrent').textContent = 'Current IP: ' + (settings.currentIp || 'not connected');
|
||||
syncNetworkMode();
|
||||
window.dispatchEvent(new CustomEvent('app:settings-loaded', {detail: {settings}}));
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
@@ -499,9 +600,9 @@ async function saveNetwork() {
|
||||
setBusy('networkSaveBtn', true, 'Saving');
|
||||
try {
|
||||
const body = {hostname: el('netHost').value, dhcp: el('netDhcp').value == 'true', ip: el('netIp').value, gateway: el('netGateway').value, subnet: el('netSubnet').value, dns1: el('netDns1').value, dns2: el('netDns2').value};
|
||||
const text = await req('POST', '/api/settings', body);
|
||||
const text = await req('POST', '/api/network', body);
|
||||
el('networkOut').textContent = text + '\nNetwork changes apply on the next WiFi reconnect or reboot.';
|
||||
if (JSON.parse(text).success) await loadSettings();
|
||||
if (JSON.parse(text).success) await loadNetworkSettings();
|
||||
} catch (error) {
|
||||
el('networkOut').textContent = 'Network save failed: ' + error.message;
|
||||
} finally {
|
||||
@@ -545,13 +646,12 @@ async function runOta() {
|
||||
|
||||
function bindEvents() {
|
||||
el('loginBtn').addEventListener('click', doLogin);
|
||||
el('logoutBtn').addEventListener('click', doLogout);
|
||||
document.querySelectorAll('.tab[data-tab]').forEach(button => button.addEventListener('click', () => showTab(button.dataset.tab)));
|
||||
document.querySelectorAll('.subtab').forEach(button => button.addEventListener('click', () => showAccessTab(button.dataset.subtab)));
|
||||
el('usersRefresh').addEventListener('click', loadUsers);
|
||||
el('userNew').addEventListener('click', newUserRecord);
|
||||
el('userBack').addEventListener('click', showUserOverview);
|
||||
el('userSave').addEventListener('click', saveSelectedUser);
|
||||
el('rolesRefresh').addEventListener('click', loadRoles);
|
||||
el('roleNew').addEventListener('click', newRoleRecord);
|
||||
el('roleBack').addEventListener('click', showRoleOverview);
|
||||
el('roleSave').addEventListener('click', addRole);
|
||||
@@ -582,6 +682,7 @@ function bindEvents() {
|
||||
el('otaRun').addEventListener('click', runOta);
|
||||
el('configDownload').addEventListener('click', downloadConfig);
|
||||
el('restoreConfigBtn').addEventListener('click', restoreConfig);
|
||||
el('restartDevice').addEventListener('click', restartDevice);
|
||||
el('factoryReset').addEventListener('click', factoryResetDevice);
|
||||
el('fileList').addEventListener('click', event => {
|
||||
const button = event.target.closest('[data-download]');
|
||||
@@ -596,4 +697,5 @@ function bindEvents() {
|
||||
el('logsClear').addEventListener('click', clearLogs);
|
||||
}
|
||||
|
||||
loadAppInfo();
|
||||
bindEvents();
|
||||
|
||||
Reference in New Issue
Block a user