feature: change all navigation to path based (#384)

* feature: change all navigation to path based

* bugfix: load default dashboard if actions is empty
This commit is contained in:
James Read 2024-08-14 12:35:10 +01:00 committed by GitHub
parent 37160a91f3
commit 9ca1940834
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 184 additions and 122 deletions

View File

@ -135,11 +135,24 @@ func startWebUIServer(cfg *config.Config) {
setupCustomWebuiDir()
mux := http.NewServeMux()
mux.Handle("/", http.FileServer(http.Dir(findWebuiDir())))
mux.Handle("/custom-webui/", http.StripPrefix("/custom-webui/", http.FileServer(http.Dir(findCustomWebuiDir()))))
mux.HandleFunc("/theme.css", generateThemeCss)
mux.HandleFunc("/webUiSettings.json", generateWebUISettings)
webuiDir := findWebuiDir()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
dirName := path.Dir(r.URL.Path)
// Mangle requests for any path like /logs or /config to load the webui index.html
if path.Ext(r.URL.Path) == "" && r.URL.Path != "/" {
log.Infof("Mangling request for %s to /index.html", r.URL.Path)
http.ServeFile(w, r, path.Join(webuiDir, "index.html"))
} else {
http.StripPrefix(dirName, http.FileServer(http.Dir(webuiDir))).ServeHTTP(w, r)
}
})
srv := &http.Server{
Addr: cfg.ListenAddressWebUI,
Handler: mux,

View File

@ -8,8 +8,8 @@
<title>OliveTin</title>
<link rel = "stylesheet" type = "text/css" href = "style.css" />
<link rel = "stylesheet" type = "text/css" href = "theme.css" />
<link rel = "stylesheet" type = "text/css" href = "/style.css" />
<link rel = "stylesheet" type = "text/css" href = "/theme.css" />
<link rel = "stylesheet" href = "node_modules/@xterm/xterm/css/xterm.css" />
<link rel = "shortcut icon" type = "image/png" href = "OliveTinLogo.png" />
@ -17,6 +17,8 @@
<link rel = "apple-touch-icon" sizes="57x57" href="OliveTinLogo-57px.png" />
<link rel = "apple-touch-icon" sizes="120x120" href="OliveTinLogo-120px.png" />
<link rel = "apple-touch-icon" sizes="180x180" href="OliveTinLogo-180px.png" />
<base href = "/" />
</head>
<body>
@ -28,16 +30,16 @@
<nav>
<ul id = "navigation-links">
<li title = "Actions">
<a id = "showActions" href = "#Actions">Actions</a>
<a id = "showActions">Actions</a>
</li>
</ul>
<ul id = "supplemental-links">
<li title = "Diagnostics">
<a id = "showDiagnostics" href = "#Diagnostics">Diagnostics</a>
<a id = "showDiagnostics">Diagnostics</a>
</li>
<li title = "Logs">
<a id = "showLogs" href = "#Logs">Logs</a>
<a id = "showLogs">Logs</a>
</li>
</ul>
</nav>
@ -64,7 +66,6 @@
</section>
<section id = "contentDiagnostics" title = "Diagnostics" hidden>
<h2>Diagnostics</h2>
<div id = "diagnostics" class = "ta-left">
<p><strong>Note:</strong> Diagnostics are only generated on OliveTin startup - they are not updated in real-time or when you refresh this page. They are intended as a "quick reference" to help you.</p>
<p>If you are having problems with OliveTin and want to raise a support request, please don't take a screenshot or copy text from this page, but instead it is highly recommended to include a <a href = "https://docs.olivetin.app/sosreport.html">sosreport</a> which is more detailed, and makes it easier to help you.</p>
@ -181,7 +182,7 @@
<td class = "timestamp">?</td>
<td>
<span role = "img" class = "icon"></span>
<a href = "#" class = "content">?</a>
<a href = "javascript:void(0)" class = "content">?</a>
<div class = "tags"></div>
</td>

View File

@ -6,16 +6,16 @@ import { ActionStatusDisplay } from './ActionStatusDisplay.js'
* This is a weird function that just sets some globals.
*/
export function initMarshaller () {
window.changeDirectory = changeDirectory
window.showSection = showSection
window.showSectionView = showSectionView
window.executionDialog = new ExecutionDialog()
window.logEntries = {}
window.registeredPaths = new Map()
window.breadcrumbNavigation = []
window.initialHash = window.location.hash
window.currentSection = ''
window.currentPath = ''
window.addEventListener('EventExecutionFinished', onExecutionFinished)
window.addEventListener('EventOutputChunk', onOutputChunk)
@ -27,8 +27,6 @@ export function marshalDashboardComponentsJsonToHtml (json) {
document.getElementById('username').innerText = json.authenticatedUser
changeDirectory(null)
document.body.setAttribute('initial-marshal-complete', 'true')
}
@ -110,22 +108,55 @@ function onExecutionFinished (evt) {
}
}
function showSection (title) {
title = title.replaceAll(' ', '')
function convertPathToBreadcrumb (path) {
const parts = path.split('/')
window.currentSection = title
const result = []
for (let i = 0; i < parts.length; i++) {
if (parts[i] === '') {
continue
}
result.push(parts.slice(0, i + 1).join('/'))
}
return result
}
function showSection (pathName) {
const path = window.registeredPaths.get(pathName)
if (path === undefined) {
console.warn('Section not found by path: ' + pathName)
showSection('/')
return
}
window.convertPathToBreadcrumb = convertPathToBreadcrumb
window.currentPath = pathName
window.breadcrumbNavigation = convertPathToBreadcrumb(pathName)
for (const section of document.querySelectorAll('section')) {
if (section.title === title) {
if (section.title === path.section) {
section.style.display = 'block'
} else {
section.style.display = 'none'
}
}
pushNewNavigationPath(pathName)
setSectionNavigationVisible(false)
changeDirectory(null)
showSectionView(path.view)
}
function pushNewNavigationPath (pathName) {
window.history.pushState({
path: pathName
}, null, pathName)
}
function setSectionNavigationVisible (visible) {
@ -177,57 +208,82 @@ export function setupSectionNavigation (style) {
document.body.classList.add('has-topbar')
}
document.getElementById('showActions').onclick = () => { showSection('Actions') }
document.getElementById('showDiagnostics').onclick = () => {
refreshDiagnostics()
showSection('Diagnostics')
}
document.getElementById('showLogs').onclick = () => { showSection('Logs') }
registerSection('/', 'Actions', null, document.getElementById('showActions'))
registerSection('/diagnostics', 'Diagnostics', null, document.getElementById('showDiagnostics'))
registerSection('/logs', 'Logs', null, document.getElementById('showLogs'))
}
function refreshDiagnostics () {
function registerSection (path, section, view, linkElement) {
window.registeredPaths.set(path, {
section: section,
view: view
})
if (linkElement != null) {
addLinkToSection(path, linkElement)
}
}
function addLinkToSection (pathName, element) {
const path = window.registeredPaths.get(pathName)
element.href = 'javascript:void(0)'
element.title = path.section
element.onclick = () => {
showSection(pathName)
}
}
export function refreshDiagnostics () {
document.getElementById('diagnostics-sshfoundkey').innerHTML = window.settings.SshFoundKey
document.getElementById('diagnostics-sshfoundconfig').innerHTML = window.settings.SshFoundConfig
}
function getSystemTitle (title) {
return title.replace(' ', '')
}
function marshalSingleDashboard (dashboard, nav) {
const oldsection = document.querySelector('section[title="' + getSystemTitle(dashboard.title) + '"]')
if (oldsection != null) {
oldsection.remove()
}
const section = document.createElement('section')
section.setAttribute('system-title', getSystemTitle(dashboard.title))
section.title = section.getAttribute('system-title')
const def = createFieldset('default', section)
section.appendChild(def)
document.getElementsByTagName('main')[0].appendChild(section)
marshalContainerContents(dashboard, section, def, dashboard.title)
const oldLi = nav.querySelector('li[title="' + dashboard.title + '"]')
if (oldLi != null) {
oldLi.remove()
}
const navigationA = document.createElement('a')
navigationA.title = dashboard.title
navigationA.innerText = dashboard.title
registerSection('/' + getSystemTitle(section.title), section.title, null, navigationA)
const navigationLi = document.createElement('li')
navigationLi.appendChild(navigationA)
navigationLi.title = dashboard.title
document.getElementById('navigation-links').appendChild(navigationLi)
}
function marshalDashboardStructureToHtml (json) {
const nav = document.getElementById('navigation-links')
for (const dashboard of json.dashboards) {
const oldsection = document.querySelector('section[title="' + dashboard.title.replace(' ', '') + '"]')
if (oldsection != null) {
oldsection.remove()
}
const section = document.createElement('section')
section.title = dashboard.title.replaceAll(' ', '')
const def = createFieldset('default', section)
section.appendChild(def)
document.getElementsByTagName('main')[0].appendChild(section)
marshalContainerContents(dashboard, section, def, dashboard.title)
const oldLi = nav.querySelector('li[title="' + dashboard.title + '"]')
if (oldLi != null) {
oldLi.remove()
}
const navigationA = document.createElement('a')
navigationA.title = dashboard.title
navigationA.innerText = dashboard.title
navigationA.setAttribute('href', '#' + dashboard.title.replace(' ', ''))
navigationA.onclick = () => {
showSection(dashboard.title.replace(' ', ''))
}
const navigationLi = document.createElement('li')
navigationLi.appendChild(navigationA)
navigationLi.title = dashboard.title
document.getElementById('navigation-links').appendChild(navigationLi)
marshalSingleDashboard(dashboard, nav)
}
const rootGroup = document.querySelector('#root-group')
@ -238,17 +294,17 @@ function marshalDashboardStructureToHtml (json) {
}
}
if (window.currentSection !== '') {
showSection(window.currentSection)
} else if (window.initialHash !== '' && document.body.getAttribute('initial-marshal-complete') === null) {
showSection(window.initialHash.replace('#', ''))
if (window.currentPath !== '') {
showSection(window.currentPath)
} else if (window.location.pathname !== '/' && document.body.getAttribute('initial-marshal-complete') === null) {
showSection(window.location.pathname)
} else {
if (rootGroup.querySelectorAll('action-button').length === 0 && json.dashboards.length > 0) {
nav.querySelector('li[title="Actions"]').style.display = 'none'
showSection(json.dashboards[0].title)
showSection('/' + getSystemTitle(json.dashboards[0].title))
} else {
showSection('Actions')
showSection('/')
}
}
}
@ -315,9 +371,10 @@ function marshalContainerContents (json, section, fieldset, parentDashboard) {
case 'fieldset':
marshalFieldset(item, section, parentDashboard)
break
case 'directory':
marshalDirectoryButton(item, fieldset)
marshalDirectory(item, section)
case 'directory': {
const directoryPath = marshalDirectory(item, section)
marshalDirectoryButton(item, fieldset, directoryPath)
}
break
case 'display':
marshalDisplay(item, fieldset)
@ -353,36 +410,16 @@ function createFieldset (title, parentDashboard) {
function marshalFieldset (item, section, parentDashboard) {
const fs = createFieldset(item.title, parentDashboard)
marshalContainerContents(item, section, fs)
marshalContainerContents(item, section, fs, parentDashboard)
section.appendChild(fs)
}
function changeDirectory (selected) {
function showSectionView (selected) {
if (selected === '') {
selected = null
}
if (selected === null) {
window.directoryNavigation = []
} else if (selected === '..') {
window.directoryNavigation.pop()
if (window.directoryNavigation.length > 0) {
selected = window.directoryNavigation[window.directoryNavigation.length - 1]
} else {
selected = null
}
} else {
// If the selected item is already in the nav list, pop elements until we get
// "back" to the existing nav item
while (window.directoryNavigation.includes(selected)) {
window.directoryNavigation.pop()
}
window.directoryNavigation.push(selected)
}
for (const fieldset of document.querySelectorAll('fieldset')) {
if (selected === null) {
if ((fieldset.id === 'root-group' || fieldset.getAttribute('parent-dashboard') !== '') && fieldset.children.length > 1) {
@ -399,50 +436,51 @@ function changeDirectory (selected) {
}
}
rebuildH1BreadcrumbNavigation(selected)
pushNewNavigationPath(window.currentPath)
}
function rebuildH1BreadcrumbNavigation () {
const title = document.querySelector('h1')
title.innerHTML = ''
const rootLink = createDirectoryBreadcrumb(window.pageTitle, null)
const rootLink = document.createElement('a')
rootLink.innerText = window.pageTitle
rootLink.href = 'javascript:void(0)'
rootLink.onclick = () => {
showSection('/')
}
title.appendChild(rootLink)
for (const dir of window.directoryNavigation) {
for (const pathName of window.breadcrumbNavigation) {
const sep = document.createElement('span')
sep.innerHTML = ' &raquo; '
title.append(sep)
if (dir === selected) {
title.append(selected)
} else {
title.appendChild(createDirectoryBreadcrumb(dir))
}
const path = window.registeredPaths.get(pathName)
title.appendChild(createNavigationBreadcrumbDisplay(path))
}
document.title = title.innerText
if (selected === null) {
window.history.pushState({ dir: null }, null, '#')
} else {
window.history.pushState({ dir: selected }, null, '#' + selected)
}
}
function createDirectoryBreadcrumb (title, link) {
function createNavigationBreadcrumbDisplay (path) {
const a = document.createElement('a')
a.innerText = title
a.title = title
a.href = 'javascript:void(0)'
if (typeof link === 'undefined') {
link = title
}
if (link === null) {
a.href = '#'
if (path.view === null) {
a.title = path.section
a.innerText = path.section
} else {
a.href = '#' + link
a.innerText = path.view
a.title = path.view
}
a.onclick = () => {
changeDirectory(link)
showSectionView(path.view)
}
return a
@ -460,11 +498,11 @@ function marshalDisplay (item, fieldset) {
fieldset.appendChild(display)
}
function marshalDirectoryButton (item, fieldset) {
function marshalDirectoryButton (item, fieldset, path) {
const directoryButton = document.createElement('button')
directoryButton.innerHTML = '<span class = "icon">' + item.icon + '</span> ' + item.title
directoryButton.onclick = () => {
changeDirectory(item.title)
showSection(path)
}
fieldset.appendChild(directoryButton)
@ -478,7 +516,7 @@ function marshalDirectory (item, section) {
directoryBackButton.innerHTML = window.settings.DefaultIconForBack
directoryBackButton.title = 'Go back one directory'
directoryBackButton.onclick = () => {
changeDirectory('..')
showSection('/' + section.title)
}
fs.appendChild(directoryBackButton)
@ -486,6 +524,12 @@ function marshalDirectory (item, section) {
marshalContainerContents(item, section, fs)
section.appendChild(fs)
const path = '/' + section.title + '/' + getSystemTitle(item.title)
registerSection(path, section.title, item.title, null)
return path
}
export function marshalLogsJsonToHtml (json) {
@ -531,8 +575,9 @@ export function marshalLogsJsonToHtml (json) {
window.addEventListener('popstate', (e) => {
e.preventDefault()
if (e.state != null && typeof e.state.dir !== 'undefined') {
changeDirectory(e.state.dir)
if (e.state != null && typeof e.state.path !== 'undefined') {
showSection(e.state.path)
}
})

View File

@ -15,7 +15,7 @@ function reconnectWebsocket () {
const websocketConnectionUrl = new URL(window.location.toString())
websocketConnectionUrl.hash = ''
websocketConnectionUrl.pathname += 'websocket'
websocketConnectionUrl.pathname = '/websocket'
if (window.location.protocol === 'https:') {
websocketConnectionUrl.protocol = 'wss'

View File

@ -5,7 +5,8 @@ import {
setupSectionNavigation,
marshalDashboardComponentsJsonToHtml,
marshalLogsJsonToHtml,
refreshServerConnectionLabel
refreshServerConnectionLabel,
refreshDiagnostics
} from './js/marshaller.js'
import { checkWebsocketConnection } from './js/websocket.js'
@ -123,6 +124,8 @@ function processWebuiSettingsJson (settings) {
}
window.settings = settings
refreshDiagnostics()
}
function main () {