Skip to content

Commit

Permalink
Merge pull request #1280 from FlowFuse/255-add-rows-to-ui-table-witho…
Browse files Browse the repository at this point in the history
…ut-array-structure

Table supports single object along with array of objects
  • Loading branch information
joepavitt authored Sep 18, 2024
2 parents 9f1a78b + ca82b0f commit f40e144
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 27 deletions.
20 changes: 20 additions & 0 deletions docs/nodes/widgets/ui-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ dynamic:

</TryDemo>

## Sending Data

Renders a set of data in a tabular format. Expects an input (`msg.payload`) in the format of:

```json
Expand All @@ -53,6 +55,24 @@ Renders a set of data in a tabular format. Expects an input (`msg.payload`) in t

The table will be rendered with colums `colA`, `colB` and `colC`, unless "Columns" are explicitely defined on the node, with "Auto Columns" toggled off.

You can also send a single piece of data to append to the existing table, in this case, the `ui-table` expects an input (`msg.payload`) in the format of:

```json
{
"colA": "A",
"colB": "Hello",
"colC": 3
}
```

### Clear Data

You can send an empty array to clear the table.

```json
[]
```

## Properties

<PropsTable/>
Expand Down
11 changes: 8 additions & 3 deletions nodes/widgets/locales/en-US/ui_table.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
</p>
<h3>Input</h3>
<p>
The ui-table widget requires an array of data to be sent via <code>msg.payload</code>.
The table will then render a row for each object within the array, and, by default, a
column for each property in the objects.
The ui-table widget requires an array or an object of data to be sent via <code>msg.payload</code>.
If an array is provided, the table will render a row for each object within the array, and, by default, a column for each property in the objects.
If an object is provided, the table will render a single row with columns for each property in the object.
</p>
<h3>Properties</h3>
<dl class="message-properties">
Expand All @@ -15,6 +15,11 @@ <h3>Properties</h3>
Defines the maximum number of data-rows to render in the table.
Excess rows will be available through pagination control. Set to "0" for no pagination.
</dd>
<dt>Action <span class="property-type">append | replace</span></dt>
<dd>
Determines the action taken when new data arrives, and whether the new data is appended
to the chart, or replaces the existing contents.
</dd>
<dt>Breakpoint <span class="property-type">str | num</span></dt>
<dd>
Controls when a table will render, instead, as a card, with each column from a row rendering
Expand Down
5 changes: 4 additions & 1 deletion nodes/widgets/locales/en-US/ui_table.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
"selection": "Interaction",
"search": "Search",
"showSearch": "Show",
"mobileBreakpoint": "Breakpoint"
"mobileBreakpoint": "Breakpoint",
"action": "Action",
"append": "Append",
"replace": "Replace"
},
"selection": {
"none": "None",
Expand Down
14 changes: 13 additions & 1 deletion nodes/widgets/ui_table.html
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@
value: null
},
mobileBreakpoint: { value: 'sm' },
mobileBreakpointType: { value: 'defaults' }
mobileBreakpointType: { value: 'defaults' },
action: { value: 'append' }
},
inputs: 1,
outputs: 1,
Expand All @@ -102,6 +103,10 @@
if (!validSelectionTypes.includes(this.selectionType)) {
$('#node-input-selectionType').val('none')
}
const validActionTypes = ['append', 'replace']
if (!validActionTypes.includes(this.action)) {
$('#node-input-action').val('append')
}
// if this groups parent is a subflow template, set the node-config-input-width and node-config-input-height up
// as typedInputs and hide the elementSizer (as it doesn't make sense for subflow templates)
if (RED.nodes.subflow(this.z)) {
Expand Down Expand Up @@ -315,6 +320,13 @@
<label for="node-input-maxrows"><i class="fa fa-tag"></i> <span data-i18n="ui-table.label.maxRows"></label>
<input type="number" id="node-input-maxrows">
</div>
<div class="form-row">
<label for="node-input-action" data-i18n="ui-table.label.action"></label></label>
<select id="node-input-action">
<option value="append" data-i18n="ui-table.label.append"></option>
<option value="replace" data-i18n="ui-table.label.replace"></option>
</select>
</div>
<div class="form-row">
<label for="node-input-mobileBreakpoint"><i class="fa fa-mobile"></i> <span data-i18n="ui-table.label.mobileBreakpoint"></label>
<input type="text" id="node-input-mobileBreakpoint">
Expand Down
24 changes: 21 additions & 3 deletions nodes/widgets/ui_table.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = function (RED) {

// which group are we rendering this widget
const group = RED.nodes.getNode(config.group)
const base = group.getBase() // Used for datastore

config.maxrows = parseInt(config.maxrows) || 0

Expand All @@ -29,9 +30,26 @@ module.exports = function (RED) {
group.register(node, config, {
onAction: true,
onInput: function (msg) {
// store the latest msg passed to node
datastore.save(group.getBase(), node, msg)
// do nothing else - do not pass the message on
const existingData = datastore.get(node.id) || []
const formatPayload = (value) => {
if (value !== null && typeof value !== 'undefined') {
// push object into array if user sends object instead of array
if (typeof value === 'object' && !Array.isArray(value)) {
return [value]
}
}
return value
}
let payload = formatPayload(msg?.payload)
// check if the action is to append records
if (config.action === 'append') {
payload = payload && payload.length > 0 ? [...existingData.payload || [], ...payload || []] : payload
}

datastore.save(base, node, {
...msg,
payload
})
}
})
}
Expand Down
67 changes: 48 additions & 19 deletions ui/src/widgets/ui-table/UITable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
class="nrdb-table"
:mobile="isMobile"
:class="{'nrdb-table--mobile': isMobile}"
:items="messages[id]?.payload" :return-object="true"
:items="payload || []" :return-object="true"
:items-per-page="itemsPerPage"
:headers="headers" :show-select="props.selectionType === 'checkbox'"
:search="search"
Expand Down Expand Up @@ -69,17 +69,18 @@ export default {
page: 1,
pages: 0,
rows: []
}
},
localData: []
}
},
computed: {
...mapState('data', ['messages']),
headers () {
if (this.props.autocols) {
if (this.messages[this.id]?.payload) {
if (this.localData?.length) {
// loop over data and get keys
const cols = []
for (const row of this.messages[this.id].payload) {
for (const row of this.localData) {
Object.keys(row).forEach((key) => {
if (!cols.includes(key)) {
cols.push(key)
Expand All @@ -90,9 +91,7 @@ export default {
return { key: col, title: col }
})
} else {
return [{
key: '', title: ''
}]
return []
}
} else if (this.props.columns) {
return this.props.columns.map((col) => {
Expand All @@ -112,12 +111,7 @@ export default {
}
},
rows () {
// store full set of data rows
if (this.messages[this.id]?.payload) {
return this.messages[this.id].payload
} else {
return undefined
}
return this.localData
},
itemsPerPage () {
return this.props.maxrows || 0
Expand All @@ -127,6 +121,13 @@ export default {
typeMap[col.key] = col.type
return typeMap
}, {})
},
payload () {
const value = this.messages[this.id]?.payload
return this.formatPayload(value) || []
},
isAppend () {
return this.props.action === 'append'
}
},
watch: {
Expand All @@ -147,25 +148,53 @@ export default {
}
},
created () {
this.$dataTracker(this.id)
this.$dataTracker(this.id, this.onMsgInput, this.onLoad)
},
mounted () {
this.calculatePaginatedRows()
this.updateIsMobile()
window.addEventListener('resize', this.updateIsMobile)
},
methods: {
formatPayload (value) {
if (value !== null && typeof value !== 'undefined') {
if (typeof value === 'object' && !Array.isArray(value)) {
return [value]
}
}
return value
},
onMsgInput (msg) {
const value = this.formatPayload(msg?.payload)
if (this.props.action === 'append') {
this.localData = value && value?.length > 0 ? [...this.localData || [], ...value] : value
} else {
this.localData = value
}
this.$store.commit('data/bind', {
action: this.props.action,
widgetId: this.id,
msg: {
payload: this.localData
}
})
this.calculatePaginatedRows()
},
onLoad (history) {
this.localData = []
this.onMsgInput(history)
},
calculatePaginatedRows () {
if (this.props.maxrows > 0) {
this.pagination.pages = Math.ceil(this.rows?.length / this.props.maxrows)
this.pagination.rows = this.rows?.slice(
if (this.itemsPerPage > 0) {
this.pagination.pages = Math.ceil(this.localData?.length / this.props.maxrows)
this.pagination.rows = this.localData.slice(
(this.pagination.page - 1) * this.props.maxrows,
(this.pagination.page) * this.props.maxrows
)
} else {
this.pagination.page = 1
this.pagination.pages = 0
this.pagination.rows = this.rows
this.pagination.rows = this.localData
}
},
onRowClick (row) {
Expand Down

0 comments on commit f40e144

Please sign in to comment.