diff --git a/src/opnsense/www/css/dashboard.css b/src/opnsense/www/css/dashboard.css index 0ef067e12..88242b37b 100644 --- a/src/opnsense/www/css/dashboard.css +++ b/src/opnsense/www/css/dashboard.css @@ -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; +} + +/* ----- */ diff --git a/src/opnsense/www/js/widgets/BaseTableWidget.js b/src/opnsense/www/js/widgets/BaseTableWidget.js index fcaf6e8bf..7ec53ba2a 100644 --- a/src/opnsense/www/js/widgets/BaseTableWidget.js +++ b/src/opnsense/www/js/widgets/BaseTableWidget.js @@ -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 = $(`
`) - if (this.options.headerPosition === 'top') { - this.$headerContainer = $(`
`); + this.$flextable = $(`
`) + this.$headerContainer = $(`
`); + + for (const h of this.options.headers) { + this.$headerContainer.append($(` +
${h}
+ `)); + } + this.$flextable.append(this.$headerContainer); + } else { + this.$flextable = $(`
`) + } + } + + _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 = $(`
`) for (const item of row) { $row.append($(` -
${item}
+
${item}
`)); } $table.append($row); break; case "top": - let $flextableRow = $(`
`); - for (const [h, c] of Object.entries(row)) { - if (!this.headers.has(h)) { - this.$headerContainer.append($(` -
${h}
- `)); - this.headers.add(h); - } - if (Array.isArray(c)) { - let $column = $('
'); - for (const item of c) { - $column.append($(` -
-
${item}
-
- `)); - } - $flextableRow.append($column); - } else { - $flextableRow.append($(` -
${c}
- `)); - } + let $gridRow = $(`
`); + for (const item of row) { + $gridRow.append($(` +
${item}
+ `)); + } + + $(`#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 = $('
'); - $row.append($(` -
${h}
- `)); - let $column = $('
'); - for (const item of c) { - $column.append($(` -
-
${item}
-
- `)); - } - $table.append($row.append($column)); - } else { - $table.append($(` -
-
${h}
-
${c}
-
+ if (row.length !== 2) { + break; + } + const [h, c] = row; + if (Array.isArray(c)) { + // nested column + let $row = $('
'); + $row.append($(` +
${h}
+ `)); + let $column = $('
'); + for (const item of c) { + $column.append($(` +
+
${item}
+
`)); } + $table.append($row.append($column)); + } else { + $table.append($(` +
+
${h}
+
${c}
+
+ `)); } break; } diff --git a/src/opnsense/www/js/widgets/SystemInformation.js b/src/opnsense/www/js/widgets/SystemInformation.js index c2152928f..92b8b8a21 100644 --- a/src/opnsense/www/js/widgets/SystemInformation.js +++ b/src/opnsense/www/js/widgets/SystemInformation.js @@ -63,12 +63,12 @@ export default class SystemInformation extends BaseTableWidget { value = $('').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']]: $('').prop('outerHTML')}); - rows.push({[this.translations['datetime']]: $('').prop('outerHTML')}); - rows.push({[this.translations['config']]: $('').prop('outerHTML')}); + rows.push([[this.translations['uptime']], $('').prop('outerHTML')]); + rows.push([[this.translations['datetime']], $('').prop('outerHTML')]); + rows.push([[this.translations['config']], $('').prop('outerHTML')]); super.updateTable(rows); }); }