dashboard: add widget CSS grid implementation for tables with top headers

This commit is contained in:
Stephan de Wit 2024-05-03 17:09:36 +02:00
parent 66c684b2c6
commit fdde2f9063
3 changed files with 149 additions and 92 deletions

View File

@ -181,7 +181,7 @@ div {
display: block;
margin: 2em auto;
width: 95%;
max-width: 1200px; /* XXX */
max-width: 1200px;
}
.flextable-header {
@ -198,10 +198,10 @@ div {
transition: 0.5s;
padding: 0.5em 0.5em;
border-top: solid 1px rgba(217, 79, 0, 0.15);
align-items: center
align-items: center;
}
.flextable-header .flex-row {
.flextable-header .flex-cell {
font-weight: bold;
}
@ -210,7 +210,7 @@ div {
transition: 500ms;
}
.flex-row {
.flex-cell {
text-align: left;
word-break: break-word;
}
@ -221,7 +221,7 @@ div {
width: 50%;
padding: 0;
}
.column .flex-row {
.column .flex-cell {
display: flex;
flex-flow: row wrap;
width: 100%;
@ -229,20 +229,54 @@ div {
border: 0;
border-top: rgba(217, 79, 0, 0.15);
}
.column .flex-row:hover {
.column .flex-cell:hover {
background: #F5F5F5;
transition: 500ms;
}
.flex-cell {
.flex-subcell {
width: 100%;
text-align: left;
}
.column .flex-row:not(:last-child) {
.column .flex-cell:not(:last-child) {
border-bottom: solid 1px rgba(217, 79, 0, 0.15);
}
/* ----- */
.cpu-type {
margin-bottom: 10px;
margin-top: 10px;
}
/* CSS grid responsive table */
.grid-table {
margin: 2em auto;
width: 95%;
}
.grid-header-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}
.grid-row {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
border-top: 1px solid #f7e2d6;
transition: 0.5s;
}
.grid-header {
border-top: 1px solid #f7e2d6;
font-weight: bold;
}
.grid-item {
border: 1 px solid #ccc;
padding: 4px;
text-align: center;
}
/* ----- */

View File

@ -38,18 +38,18 @@ export default class BaseTableWidget extends BaseWidget {
this.sizeStates = {
0: {
'.flextable-row': {'padding': ''},
'.header .flex-row': {'border-bottom': 'solid 1px'},
'.flex-row': {'width': '100%'},
'.column': {'width': '100%'},
'.flextable-header .flex-cell': {'border-bottom': 'solid 1px'},
'.flex-cell': {'width': '100%'},
'.column': {'width': '100%'},
'.flex-subcell': {'width': '100%'},
},
500: {
400: {
'.flextable-row': {'padding': '0.5em 0.5em'},
'.header .flex-row': {'border-bottom': ''},
'.flex-row': {'width': this._calculateColumnWidth.bind(this)},
'.column .flex-row': {'width': '100%'},
'.flextable-header .flex-cell': {'border-bottom': ''},
'.flex-cell': {'width': this._calculateColumnWidth.bind(this)},
'.column .flex-cell': {'width': '100%'},
'.column': {'width': ''},
'.flex-cell': {'width': ''},
'.flex-subcell': {'width': ''},
}
}
this.widths = Object.keys(this.sizeStates).sort();
@ -57,7 +57,6 @@ export default class BaseTableWidget extends BaseWidget {
this.flextableId = Math.random().toString(36).substring(7);
this.$flextable = null;
this.$headerContainer = null;
this.headers = new Set();
}
_calculateColumnWidth() {
@ -65,8 +64,6 @@ export default class BaseTableWidget extends BaseWidget {
switch (this.options.headerPosition) {
case 'none':
return `calc(100% / ${this.data[0].length})`;
case 'top':
return `calc(100% / ${Object.keys(this.data[0]).length})`;
case 'left':
return `calc(100% / 2)`;
}
@ -81,109 +78,135 @@ export default class BaseTableWidget extends BaseWidget {
return null;
}
this.$flextable = $(`<div class="flextable-container" id="${this.flextableId}" role="table"></div>`)
if (this.options.headerPosition === 'top') {
this.$headerContainer = $(`<div class="flextable-header"></div>`);
this.$flextable = $(`<div class="grid-table" id="id_${this.flextableId}" role="table"></div>`)
this.$headerContainer = $(`<div id="header_${this.flextableId}" class="grid-header-container"></div>`);
for (const h of this.options.headers) {
this.$headerContainer.append($(`
<div class="grid-item grid-header">${h}</div>
`));
}
this.$flextable.append(this.$headerContainer);
} else {
this.$flextable = $(`<div class="flextable-container" id="id_${this.flextableId}" role="table"></div>`)
}
}
_rotate(arr, newElement) {
arr.unshift(newElement);
if (arr.length > this.options.rotation) {
arr.splice(this.options.rotation);
}
const divs = document.querySelectorAll(`#id_${this.flextableId} .grid-row`);
if (divs.length > this.options.rotation) {
for (let i = this.options.rotation; i < divs.length; i++) {
$(divs[i]).remove();
}
}
}
setTableOptions(options = {}) {
/**
* headerPosition: top, left or none.
* top: headers are on top of the table. Data layout: [{header1: value1}, {header1: value2}, ...]
* left: headers are on the left of the table (key-value). Data layout: [{header1: value1}, {header2: value2}, ...].
* Supports nested columns (e.g. {header1: [value1, value2...]})
* none: no headers. Data layout: [[value1, value2], ...]
* top: headers are on top of the table. Headers are defined in the options. Data layout:
* [
* ['x', 'y', 'z'],
* ['x', 'y', 'z']
* ]
*
* left: headers are on the left of the table (key-value). Data layout:
* [
* ['x', 'x1'],
* ['y', 'y1'],
* ['z', ['z1', 'z2']] <-- supports nested columns
* ]
*
* none: no headers, same data layout as 'top', without headers set as an option.
*
* rotation: limit table entries to a certain amount, and rotate them. Only applicable for headerPosition: top.
* headers: list of headers to display. Only applicable for headerPosition: top.
*/
this.options = {
headerPosition: 'top',
rotation: false,
...options // merge and override defaults
}
}
updateTable(data = []) {
let $table = $(`#${this.flextableId}`);
let $table = $(`#id_${this.flextableId}`);
$table.children('.flextable-row').remove();
this.data = data;
if (!this.options.rotation) {
$table.children('.flextable-row').remove();
this.data = data;
}
for (const row of data) {
let rowType = Array.isArray(row) && row !== null ? 'flat' : 'nested';
if (rowType === 'flat' && this.options.headerPosition !== 'none') {
console.error('Flat data is not supported with headers');
return null;
}
if (rowType === 'nested' && this.options.headerPosition === 'none') {
console.error('Nested data requires headers');
return null;
}
switch (this.options.headerPosition) {
case "none":
let $row = $(`<div class="flextable-row"></div>`)
for (const item of row) {
$row.append($(`
<div class="flex-row" role="cell">${item}</div>
<div class="flex-cell" role="cell">${item}</div>
`));
}
$table.append($row);
break;
case "top":
let $flextableRow = $(`<div class="flextable-row"></div>`);
for (const [h, c] of Object.entries(row)) {
if (!this.headers.has(h)) {
this.$headerContainer.append($(`
<div class="flex-row">${h}</div>
`));
this.headers.add(h);
}
if (Array.isArray(c)) {
let $column = $('<div class="column"></div>');
for (const item of c) {
$column.append($(`
<div class="flex-row">
<div class="flex-cell">${item}</div>
</div>
`));
}
$flextableRow.append($column);
} else {
$flextableRow.append($(`
<div class="flex-row">${c}</div>
`));
}
let $gridRow = $(`<div class="grid-row" style="opacity: 0.4; background-color: #f7e2d6"></div>`);
for (const item of row) {
$gridRow.append($(`
<div class="grid-item">${item}</div>
`));
}
$(`#header_${this.flextableId}`).after($gridRow);
if (this.options.rotation) {
$gridRow.animate({
from: 0,
to: 255,
opacity: 1,
}, {
duration: 50,
easing: 'linear',
step: function(now) {
$gridRow.css('background-color',`transparent`)
}
});
this._rotate(this.data, row);
}
$table.append($flextableRow);
break;
case "left":
for (const [h, c] of Object.entries(row)) {
if (Array.isArray(c)) {
// nested column
let $row = $('<div class="flextable-row"></div>');
$row.append($(`
<div class="flex-row rowspan first"><b>${h}</b></div>
`));
let $column = $('<div class="column"></div>');
for (const item of c) {
$column.append($(`
<div class="flex-row">
<div class="flex-cell">${item}</div>
</div>
`));
}
$table.append($row.append($column));
} else {
$table.append($(`
<div class="flextable-row">
<div class="flex-row first"><b>${h}</b></div>
<div class="flex-row">${c}</div>
</div>
if (row.length !== 2) {
break;
}
const [h, c] = row;
if (Array.isArray(c)) {
// nested column
let $row = $('<div class="flextable-row"></div>');
$row.append($(`
<div class="flex-cell rowspan first"><b>${h}</b></div>
`));
let $column = $('<div class="column"></div>');
for (const item of c) {
$column.append($(`
<div class="flex-cell">
<div class="flex-subcell">${item}</div>
</div>
`));
}
$table.append($row.append($column));
} else {
$table.append($(`
<div class="flextable-row">
<div class="flex-cell first"><b>${h}</b></div>
<div class="flex-cell">${c}</div>
</div>
`));
}
break;
}

View File

@ -63,12 +63,12 @@ export default class SystemInformation extends BaseTableWidget {
value = $('<a>').attr('href', '/ui/core/firmware#checkupdate').text(value).prop('outerHTML');
}
rows.push({[this.translations[key]]: value});
rows.push([[this.translations[key]], value]);
}
rows.push({[this.translations['uptime']]: $('<span id="uptime">').prop('outerHTML')});
rows.push({[this.translations['datetime']]: $('<span id="datetime">').prop('outerHTML')});
rows.push({[this.translations['config']]: $('<span id="config">').prop('outerHTML')});
rows.push([[this.translations['uptime']], $('<span id="uptime">').prop('outerHTML')]);
rows.push([[this.translations['datetime']], $('<span id="datetime">').prop('outerHTML')]);
rows.push([[this.translations['config']], $('<span id="config">').prop('outerHTML')]);
super.updateTable(rows);
});
}