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($(`
+
+ `));
+ }
+
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($(`
-
- `));
- }
- $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($(`
-
- `));
- }
- $table.append($row.append($column));
- } else {
- $table.append($(`
-
+ 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($(`
+
`));
}
+ $table.append($row.append($column));
+ } else {
+ $table.append($(`
+
+ `));
}
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);
});
}