Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Table supports single object along with array of objects #1280

Merged
merged 7 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
joepavitt marked this conversation as resolved.
Show resolved Hide resolved
: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 []
joepavitt marked this conversation as resolved.
Show resolved Hide resolved
}
} 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
Loading