feature: Dashboards, at long last (#224)
* feature: Dashboards, at long last * fmt: action button IDs now use hypens. Removed ;
This commit is contained in:
parent
1b13a2bc4b
commit
6892a679ee
|
|
@ -10,7 +10,7 @@ message Action {
|
||||||
string icon = 3;
|
string icon = 3;
|
||||||
bool can_exec = 4;
|
bool can_exec = 4;
|
||||||
repeated ActionArgument arguments = 5;
|
repeated ActionArgument arguments = 5;
|
||||||
bool popup_on_start = 6;
|
string popup_on_start = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ActionArgument {
|
message ActionArgument {
|
||||||
|
|
@ -39,10 +39,18 @@ message GetDashboardComponentsResponse {
|
||||||
string title = 1;
|
string title = 1;
|
||||||
repeated Action actions = 2;
|
repeated Action actions = 2;
|
||||||
repeated Entity entities = 3;
|
repeated Entity entities = 3;
|
||||||
|
repeated DashboardItem dashboards = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetDashboardComponentsRequest {}
|
message GetDashboardComponentsRequest {}
|
||||||
|
|
||||||
|
message DashboardItem {
|
||||||
|
string title = 1;
|
||||||
|
string type = 2;
|
||||||
|
repeated DashboardItem contents = 3;
|
||||||
|
string link = 4;
|
||||||
|
}
|
||||||
|
|
||||||
message StartActionRequest {
|
message StartActionRequest {
|
||||||
string action_name = 1;
|
string action_name = 1;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,15 @@ describe('config: multipleDropdowns', function () {
|
||||||
|
|
||||||
it('Multiple dropdowns are possible', async function() {
|
it('Multiple dropdowns are possible', async function() {
|
||||||
await webdriver.get(runner.baseUrl())
|
await webdriver.get(runner.baseUrl())
|
||||||
await webdriver.manage().setTimeouts({ implicit: 2000 });
|
await webdriver.manage().setTimeouts({ implicit: 2000 })
|
||||||
|
|
||||||
const button = await webdriver.findElement(By.id('actionButton_bdc45101bbd12c1397557790d9f3e059')).findElement(By.tagName('button'));
|
const button = await webdriver.findElement(By.id('actionButton-bdc45101bbd12c1397557790d9f3e059')).findElement(By.tagName('button'))
|
||||||
|
|
||||||
expect(button).to.not.be.undefined;
|
expect(button).to.not.be.undefined
|
||||||
|
|
||||||
await button.click()
|
await button.click()
|
||||||
|
|
||||||
const dialog = await webdriver.findElement(By.id('argument-popup'));
|
const dialog = await webdriver.findElement(By.id('argument-popup'))
|
||||||
|
|
||||||
await webdriver.wait(until.elementIsVisible(dialog), 2000)
|
await webdriver.wait(until.elementIsVisible(dialog), 2000)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ type Action struct {
|
||||||
ExecOnFileChangedInDir []string
|
ExecOnFileChangedInDir []string
|
||||||
MaxConcurrent int
|
MaxConcurrent int
|
||||||
Arguments []ActionArgument
|
Arguments []ActionArgument
|
||||||
PopupOnStart bool
|
PopupOnStart string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActionArgument objects appear on Actions.
|
// ActionArgument objects appear on Actions.
|
||||||
|
|
@ -72,6 +72,7 @@ type Config struct {
|
||||||
ExternalRestAddress string
|
ExternalRestAddress string
|
||||||
LogLevel string
|
LogLevel string
|
||||||
Actions []Action `mapstructure:"actions"`
|
Actions []Action `mapstructure:"actions"`
|
||||||
|
Dashboards []DashboardItem `mapstructure:"dashboards"`
|
||||||
Entities []Entity `mapstructure:"entities"`
|
Entities []Entity `mapstructure:"entities"`
|
||||||
CheckForUpdates bool
|
CheckForUpdates bool
|
||||||
PageTitle string
|
PageTitle string
|
||||||
|
|
@ -89,6 +90,14 @@ type Config struct {
|
||||||
AccessControlLists []AccessControlList
|
AccessControlLists []AccessControlList
|
||||||
WebUIDir string
|
WebUIDir string
|
||||||
CronSupportForSeconds bool
|
CronSupportForSeconds bool
|
||||||
|
SectionNavigationStyle string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardItem struct {
|
||||||
|
Title string
|
||||||
|
Type string
|
||||||
|
Link string
|
||||||
|
Contents []DashboardItem
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfig gets a new Config structure with sensible default values.
|
// DefaultConfig gets a new Config structure with sensible default values.
|
||||||
|
|
@ -112,6 +121,7 @@ func DefaultConfig() *Config {
|
||||||
config.AuthJwtClaimUserGroup = "group"
|
config.AuthJwtClaimUserGroup = "group"
|
||||||
config.WebUIDir = "./webui"
|
config.WebUIDir = "./webui"
|
||||||
config.CronSupportForSeconds = false
|
config.CronSupportForSeconds = false
|
||||||
|
config.SectionNavigationStyle = "sidebar"
|
||||||
|
|
||||||
return &config
|
return &config
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,8 @@ func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashb
|
||||||
|
|
||||||
log.Tracef("GetDashboardComponents: %v", res)
|
log.Tracef("GetDashboardComponents: %v", res)
|
||||||
|
|
||||||
|
dashboardCfgToPb(res, cfg.Dashboards)
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package grpcapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
pb "github.com/OliveTin/OliveTin/gen/grpc"
|
||||||
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func dashboardCfgToPb(res *pb.GetDashboardComponentsResponse, dashboards []config.DashboardItem) {
|
||||||
|
for _, dashboard := range dashboards {
|
||||||
|
res.Dashboards = append(res.Dashboards, &pb.DashboardItem{
|
||||||
|
Type: "dashboard",
|
||||||
|
Title: dashboard.Title,
|
||||||
|
Contents: getDashboardContents(&dashboard),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDashboardContents(dashboard *config.DashboardItem) []*pb.DashboardItem {
|
||||||
|
ret := make([]*pb.DashboardItem, 0)
|
||||||
|
|
||||||
|
for _, subitem := range dashboard.Contents {
|
||||||
|
newitem := &pb.DashboardItem{
|
||||||
|
Title: subitem.Title,
|
||||||
|
Type: subitem.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(subitem.Contents) > 0 {
|
||||||
|
if newitem.Type != "fieldset" {
|
||||||
|
newitem.Type = "directory"
|
||||||
|
}
|
||||||
|
|
||||||
|
newitem.Contents = getDashboardContents(&subitem)
|
||||||
|
} else {
|
||||||
|
newitem.Type = "link"
|
||||||
|
newitem.Link = subitem.Link
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, newitem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ type webUISettings struct {
|
||||||
AvailableVersion string
|
AvailableVersion string
|
||||||
CurrentVersion string
|
CurrentVersion string
|
||||||
PageTitle string
|
PageTitle string
|
||||||
|
SectionNavigationStyle string
|
||||||
}
|
}
|
||||||
|
|
||||||
func findWebuiDir() string {
|
func findWebuiDir() string {
|
||||||
|
|
@ -57,6 +58,7 @@ func generateWebUISettings(w http.ResponseWriter, r *http.Request) {
|
||||||
AvailableVersion: updatecheck.AvailableVersion,
|
AvailableVersion: updatecheck.AvailableVersion,
|
||||||
CurrentVersion: updatecheck.CurrentVersion,
|
CurrentVersion: updatecheck.CurrentVersion,
|
||||||
PageTitle: cfg.PageTitle,
|
PageTitle: cfg.PageTitle,
|
||||||
|
SectionNavigationStyle: cfg.SectionNavigationStyle,
|
||||||
})
|
})
|
||||||
|
|
||||||
_, err := w.Write([]byte(jsonRet))
|
_, err := w.Write([]byte(jsonRet))
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,16 @@
|
||||||
<h1 id = "page-title">OliveTin</h1>
|
<h1 id = "page-title">OliveTin</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id = "content-sidebar">
|
|
||||||
<input type = "checkbox" id = "hide-sidebar-checkbox" hidden checked />
|
<input type = "checkbox" id = "hide-sidebar-checkbox" hidden checked />
|
||||||
<aside>
|
<nav hidden>
|
||||||
<ul>
|
<ul id = "navigation-links">
|
||||||
<li><a id = "showActions">Actions</a></li>
|
<li><a id = "showActions">Actions</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul id = "supplemental-links">
|
||||||
<li><a id = "showLogs">Logs</a></li>
|
<li><a id = "showLogs">Logs</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</nav>
|
||||||
</div>
|
|
||||||
|
|
||||||
<section id = "contentLogs" title = "Logs" hidden>
|
<section id = "contentLogs" title = "Logs" hidden>
|
||||||
<div class = "toolbar">
|
<div class = "toolbar">
|
||||||
|
|
@ -53,7 +53,8 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id = "contentActions" title = "Actions" hidden >
|
<section id = "contentActions" title = "Actions" hidden >
|
||||||
<fieldset id = "root-group" title = "Dashboard of buttons">
|
<fieldset id = "root-group" title = "Actions">
|
||||||
|
<legend hidden>Actions</legend>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class ActionButton extends window.HTMLElement {
|
||||||
this.domTitle.innerText = this.btn.title
|
this.domTitle.innerText = this.btn.title
|
||||||
this.domIcon.innerHTML = this.unicodeIcon
|
this.domIcon.innerHTML = this.unicodeIcon
|
||||||
|
|
||||||
this.setAttribute('id', 'actionButton_' + json.id)
|
this.setAttribute('id', 'actionButton-' + json.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFromJson (json) {
|
updateFromJson (json) {
|
||||||
|
|
@ -68,7 +68,6 @@ class ActionButton extends window.HTMLElement {
|
||||||
startAction (actionArgs) {
|
startAction (actionArgs) {
|
||||||
// this.btn.disabled = true
|
// this.btn.disabled = true
|
||||||
// this.isWaiting = true
|
// this.isWaiting = true
|
||||||
// this.updateDom()
|
|
||||||
this.btn.classList = [] // Removes old animation classes
|
this.btn.classList = [] // Removes old animation classes
|
||||||
|
|
||||||
if (actionArgs === undefined) {
|
if (actionArgs === undefined) {
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,321 @@
|
||||||
import './ActionButton.js' // To define action-button
|
import './ActionButton.js' // To define action-button
|
||||||
|
|
||||||
export function marshalActionButtonsJsonToHtml (json) {
|
export function marshalDashboardComponentsJsonToHtml (json) {
|
||||||
|
marshalActionsJsonToHtml(json)
|
||||||
|
marshalDashboardStructureToHtml(json)
|
||||||
|
|
||||||
|
window.changeDirectory = changeDirectory
|
||||||
|
window.showSection = showSection
|
||||||
|
|
||||||
|
changeDirectory(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function marshalActionsJsonToHtml (json) {
|
||||||
const currentIterationTimestamp = Date.now()
|
const currentIterationTimestamp = Date.now()
|
||||||
|
|
||||||
for (const jsonButton of json.actions) {
|
window.actionButtons = {}
|
||||||
let htmlButton = document.querySelector('#execution-' + jsonButton.id)
|
|
||||||
|
|
||||||
if (htmlButton == null) {
|
for (const jsonButton of json.actions) {
|
||||||
|
let htmlButton = window.actionButtons[jsonButton.id]
|
||||||
|
|
||||||
|
if (typeof htmlButton === 'undefined') {
|
||||||
htmlButton = document.createElement('action-button')
|
htmlButton = document.createElement('action-button')
|
||||||
htmlButton.constructFromJson(jsonButton)
|
htmlButton.constructFromJson(jsonButton)
|
||||||
|
|
||||||
document.getElementById('root-group').appendChild(htmlButton)
|
window.actionButtons[jsonButton.title] = htmlButton
|
||||||
} else {
|
|
||||||
htmlButton.updateFromJson(jsonButton)
|
|
||||||
htmlButton.updateDom()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
htmlButton.updateFromJson(jsonButton)
|
||||||
htmlButton.updateIterationTimestamp = currentIterationTimestamp
|
htmlButton.updateIterationTimestamp = currentIterationTimestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove existing, but stale buttons (that were not updated in this round)
|
// Remove existing, but stale buttons (that were not updated in this round)
|
||||||
for (const existingButton of document.querySelector('#contentActions').querySelectorAll('action-button')) {
|
for (const existingButton of document.querySelectorAll('action-button')) {
|
||||||
if (existingButton.updateIterationTimestamp !== currentIterationTimestamp) {
|
if (existingButton.updateIterationTimestamp !== currentIterationTimestamp) {
|
||||||
existingButton.remove()
|
existingButton.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showSection (title) {
|
||||||
|
for (const section of document.querySelectorAll('section')) {
|
||||||
|
if (section.title === title) {
|
||||||
|
section.style.display = 'block'
|
||||||
|
} else {
|
||||||
|
section.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const otherName of ['Actions', 'Logs']) {
|
||||||
|
document.getElementById('show' + otherName).classList.remove('activeSection')
|
||||||
|
document.getElementById('content' + otherName).hidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// document.getElementById('show' + name).classList.add('activeSection')
|
||||||
|
// document.getElementById('content' + name).hidden = false
|
||||||
|
|
||||||
|
document.getElementById('hide-sidebar-checkbox').checked = true
|
||||||
|
|
||||||
|
changeDirectory(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupSectionNavigation (style) {
|
||||||
|
const nav = document.querySelector('nav')
|
||||||
|
|
||||||
|
if (style === 'sidebar') {
|
||||||
|
nav.classList += 'sidebar'
|
||||||
|
|
||||||
|
document.body.classList += 'has-sidebar'
|
||||||
|
} else {
|
||||||
|
nav.classList += 'topbar'
|
||||||
|
|
||||||
|
document.body.classList += 'has-topbar'
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.hidden = false
|
||||||
|
|
||||||
|
document.getElementById('showActions').onclick = () => { showSection('Actions') }
|
||||||
|
document.getElementById('showLogs').onclick = () => { showSection('Logs') }
|
||||||
|
}
|
||||||
|
|
||||||
|
function marshalDashboardStructureToHtml (json) {
|
||||||
|
const nav = document.getElementById('navigation-links')
|
||||||
|
|
||||||
|
for (const dashboard of json.dashboards) {
|
||||||
|
const oldsection = document.querySelector('section[title="' + dashboard.title + '"]')
|
||||||
|
|
||||||
|
if (oldsection != null) {
|
||||||
|
oldsection.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
const section = document.createElement('section')
|
||||||
|
section.title = dashboard.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
|
||||||
|
navigationA.onclick = () => {
|
||||||
|
showSection(dashboard.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationLi = document.createElement('li')
|
||||||
|
navigationLi.appendChild(navigationA)
|
||||||
|
navigationLi.title = dashboard.title
|
||||||
|
|
||||||
|
document.getElementById('navigation-links').appendChild(navigationLi)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.dashboards.length === 0) {
|
||||||
|
showSection('Actions')
|
||||||
|
} else {
|
||||||
|
showSection(json.dashboards[0].title)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootGroup = document.querySelector('#root-group')
|
||||||
|
|
||||||
|
let hasRootActions = false
|
||||||
|
|
||||||
|
for (const btn of Object.values(window.actionButtons)) {
|
||||||
|
if (btn.parentElement === null) {
|
||||||
|
rootGroup.appendChild(btn)
|
||||||
|
hasRootActions = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasRootActions) {
|
||||||
|
nav.querySelector('li[title="Actions"]').style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function marshalLink (item, fieldset) {
|
||||||
|
let btn = window.actionButtons[item.link]
|
||||||
|
|
||||||
|
if (typeof btn === 'undefined') {
|
||||||
|
btn = document.createElement('button')
|
||||||
|
btn.innerText = 'Action not found: ' + item.link
|
||||||
|
btn.classList.add('error')
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset.appendChild(btn)
|
||||||
|
}
|
||||||
|
|
||||||
|
function marshalContainerContents (json, section, fieldset, parentDashboard) {
|
||||||
|
for (const item of json.contents) {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'fieldset':
|
||||||
|
marshalFieldset(item, section, parentDashboard)
|
||||||
|
break
|
||||||
|
case 'directory':
|
||||||
|
marshalDirectoryButton(item, fieldset)
|
||||||
|
marshalDirectory(item, section)
|
||||||
|
break
|
||||||
|
case 'link':
|
||||||
|
marshalLink(item, fieldset)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFieldset (title, parentDashboard) {
|
||||||
|
const legend = document.createElement('legend')
|
||||||
|
legend.innerText = title
|
||||||
|
|
||||||
|
const fs = document.createElement('fieldset')
|
||||||
|
fs.title = title
|
||||||
|
fs.appendChild(legend)
|
||||||
|
|
||||||
|
if (typeof parentDashboard === 'undefined') {
|
||||||
|
fs.setAttribute('parent-dashboard', '')
|
||||||
|
} else {
|
||||||
|
fs.setAttribute('parent-dashboard', parentDashboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
|
function marshalFieldset (item, section, parentDashboard) {
|
||||||
|
const fs = createFieldset(item.title, parentDashboard)
|
||||||
|
|
||||||
|
marshalContainerContents(item, section, fs)
|
||||||
|
|
||||||
|
section.appendChild(fs)
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeDirectory (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) {
|
||||||
|
fieldset.style.display = 'grid'
|
||||||
|
} else {
|
||||||
|
fieldset.style.display = 'none'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (fieldset.title === selected) {
|
||||||
|
fieldset.style.display = 'grid'
|
||||||
|
} else {
|
||||||
|
fieldset.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = document.querySelector('h1')
|
||||||
|
title.innerHTML = ''
|
||||||
|
|
||||||
|
const rootLink = createDirectoryBreadcrumb('OliveTin', null)
|
||||||
|
title.appendChild(rootLink)
|
||||||
|
|
||||||
|
for (const dir of window.directoryNavigation) {
|
||||||
|
const sep = document.createElement('span')
|
||||||
|
sep.innerHTML = ' » '
|
||||||
|
title.append(sep)
|
||||||
|
|
||||||
|
if (dir === selected) {
|
||||||
|
title.append(selected)
|
||||||
|
} else {
|
||||||
|
title.appendChild(createDirectoryBreadcrumb(dir))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.title = title.innerText
|
||||||
|
|
||||||
|
if (selected === null) {
|
||||||
|
window.location.hash = null
|
||||||
|
|
||||||
|
window.history.pushState({ dir: null }, null, '#')
|
||||||
|
} else {
|
||||||
|
window.location.hash = selected
|
||||||
|
|
||||||
|
window.history.pushState({ dir: selected }, null, '#' + selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDirectoryBreadcrumb (title, link) {
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.innerText = title
|
||||||
|
a.title = title
|
||||||
|
|
||||||
|
if (typeof link === 'undefined') {
|
||||||
|
link = title
|
||||||
|
}
|
||||||
|
|
||||||
|
if (link === null) {
|
||||||
|
a.href = '#'
|
||||||
|
} else {
|
||||||
|
a.href = '#' + link
|
||||||
|
}
|
||||||
|
|
||||||
|
a.onclick = () => {
|
||||||
|
changeDirectory(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
function marshalDirectoryButton (item, fieldset) {
|
||||||
|
const directoryButton = document.createElement('button')
|
||||||
|
directoryButton.innerHTML = '<span class = "icon">📁</span> ' + item.title
|
||||||
|
directoryButton.onclick = () => {
|
||||||
|
changeDirectory(item.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset.appendChild(directoryButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
function marshalDirectory (item, section) {
|
||||||
|
const fs = createFieldset(item.title)
|
||||||
|
fs.style.display = 'none'
|
||||||
|
|
||||||
|
const directoryBackButton = document.createElement('button')
|
||||||
|
directoryBackButton.innerHTML = '«'
|
||||||
|
directoryBackButton.title = 'Go back one directory'
|
||||||
|
directoryBackButton.onclick = () => {
|
||||||
|
changeDirectory('..')
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.appendChild(directoryBackButton)
|
||||||
|
|
||||||
|
marshalContainerContents(item, section, fs)
|
||||||
|
|
||||||
|
section.appendChild(fs)
|
||||||
|
}
|
||||||
|
|
||||||
export function marshalLogsJsonToHtml (json) {
|
export function marshalLogsJsonToHtml (json) {
|
||||||
for (const logEntry of json.logs) {
|
for (const logEntry of json.logs) {
|
||||||
const tpl = document.getElementById('tplLogRow')
|
const tpl = document.getElementById('tplLogRow')
|
||||||
|
|
@ -69,3 +358,10 @@ export function marshalLogsJsonToHtml (json) {
|
||||||
document.querySelector('#logTableBody').prepend(row)
|
document.querySelector('#logTableBody').prepend(row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener('popstate', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (e.state != null && typeof e.state.dir !== 'undefined') {
|
||||||
|
changeDirectory(e.state.dir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
import { marshalActionButtonsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js'
|
import { setupSectionNavigation, marshalDashboardComponentsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js'
|
||||||
import { checkWebsocketConnection } from './js/websocket.js'
|
import { checkWebsocketConnection } from './js/websocket.js'
|
||||||
|
|
||||||
function searchLogs (e) {
|
function searchLogs (e) {
|
||||||
|
|
@ -24,25 +24,6 @@ function searchLogsClear () {
|
||||||
document.getElementById('logSearchBox').value = ''
|
document.getElementById('logSearchBox').value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSection (name) {
|
|
||||||
for (const otherName of ['Actions', 'Logs']) {
|
|
||||||
document.getElementById('show' + otherName).classList.remove('activeSection')
|
|
||||||
document.getElementById('content' + otherName).hidden = true
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('show' + name).classList.add('activeSection')
|
|
||||||
document.getElementById('content' + name).hidden = false
|
|
||||||
|
|
||||||
document.getElementById('hide-sidebar-checkbox').checked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupSections () {
|
|
||||||
document.getElementById('showActions').onclick = () => { showSection('Actions') }
|
|
||||||
document.getElementById('showLogs').onclick = () => { showSection('Logs') }
|
|
||||||
|
|
||||||
showSection('Actions')
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupLogSearchBox () {
|
function setupLogSearchBox () {
|
||||||
document.getElementById('logSearchBox').oninput = searchLogs
|
document.getElementById('logSearchBox').oninput = searchLogs
|
||||||
document.getElementById('searchLogsClear').onclick = searchLogsClear
|
document.getElementById('searchLogsClear').onclick = searchLogsClear
|
||||||
|
|
@ -91,7 +72,7 @@ function fetchGetDashboardComponents () {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.restAvailable = true
|
window.restAvailable = true
|
||||||
marshalActionButtonsJsonToHtml(res)
|
marshalDashboardComponentsJsonToHtml(res)
|
||||||
|
|
||||||
refreshServerConnectionLabel() // in-case it changed, update the label quicker
|
refreshServerConnectionLabel() // in-case it changed, update the label quicker
|
||||||
}).catch((err) => { // err is 1st arg
|
}).catch((err) => { // err is 1st arg
|
||||||
|
|
@ -142,8 +123,8 @@ function processWebuiSettingsJson (settings) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function main () {
|
function main () {
|
||||||
setupSections()
|
|
||||||
setupLogSearchBox()
|
setupLogSearchBox()
|
||||||
|
setupSectionNavigation('sidebar')
|
||||||
|
|
||||||
window.fetch('webUiSettings.json').then(res => {
|
window.fetch('webUiSettings.json').then(res => {
|
||||||
return res.json()
|
return res.json()
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,25 @@
|
||||||
body {
|
body {
|
||||||
background-color: #dee3e7;
|
background-color: #dee3e7;
|
||||||
color: black;
|
color: black;
|
||||||
text-align: center;
|
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog {
|
dialog {
|
||||||
box-shadow: 0 0 6px 0 #444;
|
box-shadow: 0 0 6px 0 #444;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
text-align: left;
|
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldset#root-group {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
grid-template-columns: repeat(auto-fit, 160px);
|
||||||
grid-template-rows: auto auto auto auto;
|
grid-template-rows: auto auto auto auto;
|
||||||
grid-gap: 1em;
|
grid-gap: 1em;
|
||||||
|
padding: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -62,48 +59,67 @@ footer a {
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside {
|
nav.sidebar {
|
||||||
|
background-color: white;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 180px;
|
width: 180px;
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
padding-top: 4em;
|
|
||||||
transition: 0.5s ease;
|
transition: 0.5s ease;
|
||||||
background-color: white;
|
|
||||||
border: 0 0 10px 0;
|
|
||||||
box-shadow: 0 0 10px 0 #444;
|
box-shadow: 0 0 10px 0 #444;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked ~ aside {
|
#navigation-links {
|
||||||
|
padding-top: 4em;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#supplemental-links {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked ~ nav.sidebar {
|
||||||
left: -250px;
|
left: -250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside ul {
|
nav ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside ul li {
|
nav ul li {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px inset black;
|
border-bottom: 1px inset black;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside ul li a {
|
nav ul li a {
|
||||||
display: block;
|
display: block;
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
padding-top: 0.5em;
|
padding-top: 0.5em;
|
||||||
padding-bottom: 0.5em;
|
padding-bottom: 0.5em;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside ul li a:hover {
|
nav ul li a:hover {
|
||||||
color: black;
|
color: black;
|
||||||
background-color: #efefef;
|
background-color: #efefef;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nav.topbar {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.topbar ul li {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
table {
|
table {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|
@ -204,8 +220,8 @@ action-button {
|
||||||
}
|
}
|
||||||
|
|
||||||
action-button button {
|
action-button button {
|
||||||
width: 100%;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -307,6 +323,9 @@ img.logo {
|
||||||
|
|
||||||
main {
|
main {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.has-sidebar main {
|
||||||
padding-top: 3em;
|
padding-top: 3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -417,6 +436,10 @@ div.toolbar * {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (width <= 600px) {
|
@media screen and (width <= 600px) {
|
||||||
|
fieldset {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin-bottom: .6em;
|
margin-bottom: .6em;
|
||||||
|
|
@ -464,7 +487,7 @@ div.toolbar * {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside {
|
nav {
|
||||||
background-color: #111;
|
background-color: #111;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
@ -474,7 +497,7 @@ div.toolbar * {
|
||||||
color: gray;
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside ul li a:hover {
|
nav ul li a:hover {
|
||||||
background-color: #666;
|
background-color: #666;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue