Просмотр исходного кода

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

* feature: change all navigation to path based

* bugfix: load default dashboard if actions is empty
James Read 1 год назад
Родитель
Сommit
9ca1940834
5 измененных файлов с 177 добавлено и 115 удалено
  1. 14 1
      internal/httpservers/webuiServer.go
  2. 8 7
      webui.dev/index.html
  3. 150 105
      webui.dev/js/marshaller.js
  4. 1 1
      webui.dev/js/websocket.js
  5. 4 1
      webui.dev/main.js

+ 14 - 1
internal/httpservers/webuiServer.go

@@ -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,

+ 8 - 7
webui.dev/index.html

@@ -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>

+ 150 - 105
webui.dev/js/marshaller.js

@@ -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('/')
+
+  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)
 
-  window.currentSection = title
+    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')
+  registerSection('/', 'Actions', null, document.getElementById('showActions'))
+  registerSection('/diagnostics', 'Diagnostics', null, document.getElementById('showDiagnostics'))
+  registerSection('/logs', 'Logs', null, document.getElementById('showLogs'))
+}
+
+function registerSection (path, section, view, linkElement) {
+  window.registeredPaths.set(path, {
+    section: section,
+    view: view
+  })
+
+  if (linkElement != null) {
+    addLinkToSection(path, linkElement)
   }
-  document.getElementById('showLogs').onclick = () => { showSection('Logs') }
 }
 
-function refreshDiagnostics () {
+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 marshalDashboardStructureToHtml (json) {
-  const nav = document.getElementById('navigation-links')
+function getSystemTitle (title) {
+  return title.replace(' ', '')
+}
 
-  for (const dashboard of json.dashboards) {
-    const oldsection = document.querySelector('section[title="' + dashboard.title.replace(' ', '') + '"]')
+function marshalSingleDashboard (dashboard, nav) {
+  const oldsection = document.querySelector('section[title="' + getSystemTitle(dashboard.title) + '"]')
 
-    if (oldsection != null) {
-      oldsection.remove()
-    }
+  if (oldsection != null) {
+    oldsection.remove()
+  }
 
-    const section = document.createElement('section')
-    section.title = dashboard.title.replaceAll(' ', '')
+  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)
+  const def = createFieldset('default', section)
+  section.appendChild(def)
 
-    document.getElementsByTagName('main')[0].appendChild(section)
-    marshalContainerContents(dashboard, section, def, dashboard.title)
+  document.getElementsByTagName('main')[0].appendChild(section)
+  marshalContainerContents(dashboard, section, def, dashboard.title)
 
-    const oldLi = nav.querySelector('li[title="' + dashboard.title + '"]')
+  const oldLi = nav.querySelector('li[title="' + dashboard.title + '"]')
 
-    if (oldLi != null) {
-      oldLi.remove()
-    }
+  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 navigationA = document.createElement('a')
+  navigationA.title = dashboard.title
+  navigationA.innerText = dashboard.title
 
-    const navigationLi = document.createElement('li')
-    navigationLi.appendChild(navigationA)
-    navigationLi.title = dashboard.title
+  registerSection('/' + getSystemTitle(section.title), section.title, null, navigationA)
 
-    document.getElementById('navigation-links').appendChild(navigationLi)
+  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) {
+    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
-
-  if (typeof link === 'undefined') {
-    link = title
-  }
+  a.href = 'javascript:void(0)'
 
-  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)
   }
 })
 

+ 1 - 1
webui.dev/js/websocket.js

@@ -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'

+ 4 - 1
webui.dev/main.js

@@ -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 () {