From 3c478f61764420e5ca37e8037e67432b4101fa45 Mon Sep 17 00:00:00 2001 From: Shawn Williams Date: Mon, 19 Oct 2026 16:03:37 -0400 Subject: [PATCH 1/2] Add tag and object filters to NetBox streams Expose tags on NetBox inventory streams, add multi-tag filtering and selectable object filters, and index tags for Collections. Update the plugin documentation and version to 1.1.0. --- plugins/NetBox/v1/dataStreams/circuits.json | 48 +++++++++++++- plugins/NetBox/v1/dataStreams/devices.json | 46 +++++++++++++- .../NetBox/v1/dataStreams/ipAddresses.json | 29 ++++++++- plugins/NetBox/v1/dataStreams/powerFeeds.json | 25 +++++++- plugins/NetBox/v1/dataStreams/prefixes.json | 46 +++++++++++++- plugins/NetBox/v1/dataStreams/racks.json | 46 +++++++++++++- .../v1/dataStreams/scripts/powerFeeds.js | 11 +++- plugins/NetBox/v1/dataStreams/scripts/tags.js | 4 ++ plugins/NetBox/v1/dataStreams/sites.json | 48 +++++++++++++- plugins/NetBox/v1/dataStreams/tags.json | 63 +++++++++++++++++++ .../v1/dataStreams/virtualMachines.json | 46 +++++++++++++- plugins/NetBox/v1/dataStreams/vlanGroups.json | 31 ++++++++- plugins/NetBox/v1/docs/README.md | 14 +++++ .../NetBox/v1/indexDefinitions/default.json | 18 ++++++ plugins/NetBox/v1/metadata.json | 2 +- 15 files changed, 466 insertions(+), 11 deletions(-) create mode 100644 plugins/NetBox/v1/dataStreams/scripts/tags.js create mode 100644 plugins/NetBox/v1/dataStreams/tags.json diff --git a/plugins/NetBox/v1/dataStreams/circuits.json b/plugins/NetBox/v1/dataStreams/circuits.json index 5e4cce84..2b3b9ef9 100644 --- a/plugins/NetBox/v1/dataStreams/circuits.json +++ b/plugins/NetBox/v1/dataStreams/circuits.json @@ -13,7 +13,15 @@ "getArgs": [ { "key": "fields", - "value": "id,cid,display,display_url,provider,provider_account,type,status,tenant,install_date,termination_date,commit_rate,description,termination_a,termination_z,created,last_updated,custom_fields" + "value": "id,cid,display,display_url,provider,provider_account,type,status,tenant,install_date,termination_date,commit_rate,description,termination_a,termination_z,created,last_updated,tags,custom_fields" + }, + { + "key": "id", + "value": "{{ (typeof circuit !== 'undefined' && circuit && circuit.length) ? circuit.map(o => [].concat(o.rawId)[0]) : null }}" + }, + { + "key": "tag", + "value": "{{ (typeof tag !== 'undefined' && tag && tag.length) ? [].concat(tag).flatMap(t => (t && typeof t === 'object') ? [t.value] : String(t).split(',')).map(t => String(t).trim()).filter(Boolean) : null }}" } ], "paging": { @@ -40,6 +48,34 @@ "expandInnerObjects": true }, "matches": "none", + "ui": [ + { + "type": "objects", + "name": "circuit", + "label": "Circuit(s) (optional)", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Circuit" + ] + } + } + }, + { + "type": "autocomplete", + "name": "tag", + "label": "Tag(s) (optional)", + "help": "Returns only objects that have all the selected tags", + "isMulti": true, + "allowCustomValues": true, + "data": { + "source": "dataStream", + "dataStreamName": "tags", + "dataSourceConfig": {} + } + } + ], "metadata": [ { "name": "id", @@ -127,6 +163,16 @@ "displayName": "Last Updated", "shape": "date" }, + { + "pattern": "^tags[.].*", + "visible": false + }, + { + "name": "tags.0.display", + "displayName": "Tags", + "shape": "string", + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + }, { "name": "display_url", "displayName": "Link", diff --git a/plugins/NetBox/v1/dataStreams/devices.json b/plugins/NetBox/v1/dataStreams/devices.json index b51378cb..3885ffe5 100644 --- a/plugins/NetBox/v1/dataStreams/devices.json +++ b/plugins/NetBox/v1/dataStreams/devices.json @@ -13,7 +13,11 @@ "getArgs": [ { "key": "fields", - "value": "id,display,display_url,status,role,device_type,platform,site,location,rack,position,face,serial,asset_tag,tenant,primary_ip,oob_ip,cluster,parent_device,description,created,last_updated,custom_fields" + "value": "id,display,display_url,status,role,device_type,platform,site,location,rack,position,face,serial,asset_tag,tenant,primary_ip,oob_ip,cluster,parent_device,description,created,last_updated,tags,custom_fields" + }, + { + "key": "id", + "value": "{{ (typeof device !== 'undefined' && device && device.length) ? device.map(o => [].concat(o.rawId)[0]) : null }}" }, { "key": "site_id", @@ -26,6 +30,10 @@ { "key": "role", "value": "{{ (typeof role !== 'undefined' && role && role.length) ? [].concat(role).flatMap(r => (r && typeof r === 'object') ? [r.value] : String(r).split(',')).map(r => String(r).trim()).filter(Boolean) : null }}" + }, + { + "key": "tag", + "value": "{{ (typeof tag !== 'undefined' && tag && tag.length) ? [].concat(tag).flatMap(t => (t && typeof t === 'object') ? [t.value] : String(t).split(',')).map(t => String(t).trim()).filter(Boolean) : null }}" } ], "paging": { @@ -53,6 +61,19 @@ }, "matches": "none", "ui": [ + { + "type": "objects", + "name": "device", + "label": "Device(s) (optional)", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Device" + ] + } + } + }, { "type": "objects", "name": "site", @@ -90,6 +111,19 @@ "dataStreamName": "deviceRoles", "dataSourceConfig": {} } + }, + { + "type": "autocomplete", + "name": "tag", + "label": "Tag(s) (optional)", + "help": "Returns only objects that have all the selected tags", + "isMulti": true, + "allowCustomValues": true, + "data": { + "source": "dataStream", + "dataStreamName": "tags", + "dataSourceConfig": {} + } } ], "metadata": [ @@ -260,6 +294,16 @@ "computed": true, "valueExpression": "{{ typeof $['last_updated'] === 'string' && $['last_updated'] ? Math.floor((Date.now() - new Date($['last_updated']).getTime()) / 86400000) : null }}" }, + { + "pattern": "^tags[.].*", + "visible": false + }, + { + "name": "tags.0.display", + "displayName": "Tags", + "shape": "string", + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + }, { "name": "display_url", "displayName": "Link", diff --git a/plugins/NetBox/v1/dataStreams/ipAddresses.json b/plugins/NetBox/v1/dataStreams/ipAddresses.json index 58e6779f..4faff3d2 100644 --- a/plugins/NetBox/v1/dataStreams/ipAddresses.json +++ b/plugins/NetBox/v1/dataStreams/ipAddresses.json @@ -13,7 +13,7 @@ "getArgs": [ { "key": "fields", - "value": "id,address,display,display_url,family,vrf,tenant,status,role,assigned_object_id,assigned_object,dns_name,description,created,last_updated,custom_fields" + "value": "id,address,display,display_url,family,vrf,tenant,status,role,assigned_object_id,assigned_object,dns_name,description,created,last_updated,tags,custom_fields" }, { "key": "assigned", @@ -38,6 +38,10 @@ { "key": "virtual_machine_id", "value": "{{ (typeof virtualMachine !== 'undefined' && virtualMachine && virtualMachine.length) ? virtualMachine.map(o => [].concat(o.rawId)[0]) : null }}" + }, + { + "key": "tag", + "value": "{{ (typeof tag !== 'undefined' && tag && tag.length) ? [].concat(tag).flatMap(t => (t && typeof t === 'object') ? [t.value] : String(t).split(',')).map(t => String(t).trim()).filter(Boolean) : null }}" } ], "paging": { @@ -146,6 +150,19 @@ "label": "SLAAC" } ] + }, + { + "type": "autocomplete", + "name": "tag", + "label": "Tag(s) (optional)", + "help": "Returns only objects that have all the selected tags", + "isMulti": true, + "allowCustomValues": true, + "data": { + "source": "dataStream", + "dataStreamName": "tags", + "dataSourceConfig": {} + } } ], "metadata": [ @@ -258,6 +275,16 @@ "displayName": "Last Updated", "shape": "date" }, + { + "pattern": "^tags[.].*", + "visible": false + }, + { + "name": "tags.0.display", + "displayName": "Tags", + "shape": "string", + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + }, { "name": "display_url", "displayName": "Link", diff --git a/plugins/NetBox/v1/dataStreams/powerFeeds.json b/plugins/NetBox/v1/dataStreams/powerFeeds.json index 6f3bf107..fc6dc73d 100644 --- a/plugins/NetBox/v1/dataStreams/powerFeeds.json +++ b/plugins/NetBox/v1/dataStreams/powerFeeds.json @@ -17,7 +17,7 @@ } ], "postBody": { - "query": "query PowerFeeds($offset: Int = 0) { power_feed_list(pagination: { offset: $offset, limit: 500 }{{ (typeof site !== 'undefined' && site && site.length) ? ', filters: { power_panel: { site: { id: { in_list: [' + site.map(s => Number([].concat(s.rawId)[0])).join(', ') + '] } } } }' : '' }}) { id name description status type supply phase voltage amperage max_utilization available_power mark_connected cable { id } tenant { name } rack { id name } power_panel { id name site { id name } } created last_updated custom_field_data } }" + "query": "query PowerFeeds($offset: Int = 0) { power_feed_list(pagination: { offset: $offset, limit: 500 }{{ (typeof site !== 'undefined' && site && site.length) ? ', filters: { power_panel: { site: { id: { in_list: [' + site.map(s => Number([].concat(s.rawId)[0])).join(', ') + '] } } } }' : '' }}) { id name description status type supply phase voltage amperage max_utilization available_power mark_connected cable { id } tenant { name } rack { id name } power_panel { id name site { id name } } created last_updated tags { name slug display } custom_field_data } }" }, "paging": { "mode": "offset", @@ -54,6 +54,19 @@ ] } } + }, + { + "type": "autocomplete", + "name": "tag", + "label": "Tag(s) (optional)", + "help": "Returns only objects that have all the selected tags", + "isMulti": true, + "allowCustomValues": true, + "data": { + "source": "dataStream", + "dataStreamName": "tags", + "dataSourceConfig": {} + } } ], "metadata": [ @@ -192,6 +205,16 @@ "displayName": "Last Updated", "shape": "date" }, + { + "pattern": "^tags[.].*", + "visible": false + }, + { + "name": "tags.0.display", + "displayName": "Tags", + "shape": "string", + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + }, { "name": "rack.id", "visible": false diff --git a/plugins/NetBox/v1/dataStreams/prefixes.json b/plugins/NetBox/v1/dataStreams/prefixes.json index ef588814..49b9c80c 100644 --- a/plugins/NetBox/v1/dataStreams/prefixes.json +++ b/plugins/NetBox/v1/dataStreams/prefixes.json @@ -13,11 +13,19 @@ "getArgs": [ { "key": "fields", - "value": "id,display,display_url,prefix,family,vrf,scope_type,scope,status,role,tenant,vlan,is_pool,mark_utilized,_depth,children,description,created,last_updated,custom_fields" + "value": "id,display,display_url,prefix,family,vrf,scope_type,scope,status,role,tenant,vlan,is_pool,mark_utilized,_depth,children,description,created,last_updated,tags,custom_fields" + }, + { + "key": "id", + "value": "{{ (typeof prefix !== 'undefined' && prefix && prefix.length) ? prefix.map(o => [].concat(o.rawId)[0]) : null }}" }, { "key": "site_id", "value": "{{ (typeof site !== 'undefined' && site && site.length) ? site.map(s => [].concat(s.rawId)[0]) : null }}" + }, + { + "key": "tag", + "value": "{{ (typeof tag !== 'undefined' && tag && tag.length) ? [].concat(tag).flatMap(t => (t && typeof t === 'object') ? [t.value] : String(t).split(',')).map(t => String(t).trim()).filter(Boolean) : null }}" } ], "paging": { @@ -45,6 +53,19 @@ }, "matches": "none", "ui": [ + { + "type": "objects", + "name": "prefix", + "label": "Prefix(es) (optional)", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Prefix" + ] + } + } + }, { "type": "objects", "name": "site", @@ -57,6 +78,19 @@ ] } } + }, + { + "type": "autocomplete", + "name": "tag", + "label": "Tag(s) (optional)", + "help": "Returns only objects that have all the selected tags", + "isMulti": true, + "allowCustomValues": true, + "data": { + "source": "dataStream", + "dataStreamName": "tags", + "dataSourceConfig": {} + } } ], "metadata": [ @@ -152,6 +186,16 @@ "displayName": "Last Updated", "shape": "date" }, + { + "pattern": "^tags[.].*", + "visible": false + }, + { + "name": "tags.0.display", + "displayName": "Tags", + "shape": "string", + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + }, { "name": "display_url", "displayName": "Link", diff --git a/plugins/NetBox/v1/dataStreams/racks.json b/plugins/NetBox/v1/dataStreams/racks.json index 35556efd..618b13f8 100644 --- a/plugins/NetBox/v1/dataStreams/racks.json +++ b/plugins/NetBox/v1/dataStreams/racks.json @@ -13,11 +13,19 @@ "getArgs": [ { "key": "fields", - "value": "id,display,display_url,site,location,status,role,tenant,facility_id,serial,asset_tag,u_height,starting_unit,form_factor,device_count,powerfeed_count,description,created,last_updated,custom_fields" + "value": "id,display,display_url,site,location,status,role,tenant,facility_id,serial,asset_tag,u_height,starting_unit,form_factor,device_count,powerfeed_count,description,created,last_updated,tags,custom_fields" + }, + { + "key": "id", + "value": "{{ (typeof rack !== 'undefined' && rack && rack.length) ? rack.map(o => [].concat(o.rawId)[0]) : null }}" }, { "key": "site_id", "value": "{{ (typeof site !== 'undefined' && site && site.length) ? site.map(s => [].concat(s.rawId)[0]) : null }}" + }, + { + "key": "tag", + "value": "{{ (typeof tag !== 'undefined' && tag && tag.length) ? [].concat(tag).flatMap(t => (t && typeof t === 'object') ? [t.value] : String(t).split(',')).map(t => String(t).trim()).filter(Boolean) : null }}" } ], "paging": { @@ -45,6 +53,19 @@ }, "matches": "none", "ui": [ + { + "type": "objects", + "name": "rack", + "label": "Rack(s) (optional)", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Rack" + ] + } + } + }, { "type": "objects", "name": "site", @@ -57,6 +78,19 @@ ] } } + }, + { + "type": "autocomplete", + "name": "tag", + "label": "Tag(s) (optional)", + "help": "Returns only objects that have all the selected tags", + "isMulti": true, + "allowCustomValues": true, + "data": { + "source": "dataStream", + "dataStreamName": "tags", + "dataSourceConfig": {} + } } ], "metadata": [ @@ -158,6 +192,16 @@ "displayName": "Last Updated", "shape": "date" }, + { + "pattern": "^tags[.].*", + "visible": false + }, + { + "name": "tags.0.display", + "displayName": "Tags", + "shape": "string", + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + }, { "name": "display_url", "displayName": "Link", diff --git a/plugins/NetBox/v1/dataStreams/scripts/powerFeeds.js b/plugins/NetBox/v1/dataStreams/scripts/powerFeeds.js index 8edb8b77..0d8a501c 100644 --- a/plugins/NetBox/v1/dataStreams/scripts/powerFeeds.js +++ b/plugins/NetBox/v1/dataStreams/scripts/powerFeeds.js @@ -3,4 +3,13 @@ if (data && data.errors && data.errors.length) { throw new Error(data.errors.map((e) => e.message).join("; ")); } -result = (data && data.data && data.data.power_feed_list) || []; +// NetBox GraphQL can't require several tags at once (nested AND matches nothing), so the Tag(s) filter is applied +// here to keep the REST streams' "has all selected tags" behaviour. Paging counts the raw list, so this is safe. +const tagSlugs = [].concat((context.config && context.config.tag) || []) + .flatMap((t) => (t && typeof t === "object" ? [t.value] : String(t).split(","))) + .map((t) => String(t).trim()) + .filter(Boolean); + +result = ((data && data.data && data.data.power_feed_list) || []).filter((feed) => + tagSlugs.every((slug) => (feed.tags || []).some((t) => t.slug === slug)), +); diff --git a/plugins/NetBox/v1/dataStreams/scripts/tags.js b/plugins/NetBox/v1/dataStreams/scripts/tags.js new file mode 100644 index 00000000..73dec329 --- /dev/null +++ b/plugins/NetBox/v1/dataStreams/scripts/tags.js @@ -0,0 +1,4 @@ +result = ((data && data.results) || []).map((r) => ({ + label: r.name, + value: r.slug, +})); diff --git a/plugins/NetBox/v1/dataStreams/sites.json b/plugins/NetBox/v1/dataStreams/sites.json index a4afc14f..c661ce53 100644 --- a/plugins/NetBox/v1/dataStreams/sites.json +++ b/plugins/NetBox/v1/dataStreams/sites.json @@ -13,7 +13,15 @@ "getArgs": [ { "key": "fields", - "value": "id,display,display_url,status,region,group,tenant,facility,time_zone,description,physical_address,latitude,longitude,device_count,rack_count,prefix_count,vlan_count,circuit_count,virtualmachine_count,created,last_updated,custom_fields" + "value": "id,display,display_url,status,region,group,tenant,facility,time_zone,description,physical_address,latitude,longitude,device_count,rack_count,prefix_count,vlan_count,circuit_count,virtualmachine_count,created,last_updated,tags,custom_fields" + }, + { + "key": "id", + "value": "{{ (typeof site !== 'undefined' && site && site.length) ? site.map(o => [].concat(o.rawId)[0]) : null }}" + }, + { + "key": "tag", + "value": "{{ (typeof tag !== 'undefined' && tag && tag.length) ? [].concat(tag).flatMap(t => (t && typeof t === 'object') ? [t.value] : String(t).split(',')).map(t => String(t).trim()).filter(Boolean) : null }}" } ], "paging": { @@ -40,6 +48,34 @@ "expandInnerObjects": true }, "matches": "none", + "ui": [ + { + "type": "objects", + "name": "site", + "label": "Site(s) (optional)", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Site" + ] + } + } + }, + { + "type": "autocomplete", + "name": "tag", + "label": "Tag(s) (optional)", + "help": "Returns only objects that have all the selected tags", + "isMulti": true, + "allowCustomValues": true, + "data": { + "source": "dataStream", + "dataStreamName": "tags", + "dataSourceConfig": {} + } + } + ], "metadata": [ { "name": "id", @@ -145,6 +181,16 @@ "displayName": "Last Updated", "shape": "date" }, + { + "pattern": "^tags[.].*", + "visible": false + }, + { + "name": "tags.0.display", + "displayName": "Tags", + "shape": "string", + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + }, { "name": "display_url", "displayName": "Link", diff --git a/plugins/NetBox/v1/dataStreams/tags.json b/plugins/NetBox/v1/dataStreams/tags.json new file mode 100644 index 00000000..491c69c5 --- /dev/null +++ b/plugins/NetBox/v1/dataStreams/tags.json @@ -0,0 +1,63 @@ +{ + "name": "tags", + "displayName": "Tags", + "description": "Tags as label and slug pairs", + "tags": [ + "Inventory" + ], + "visibility": { + "type": "hidden" + }, + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "api/extras/tags/", + "getArgs": [ + { + "key": "fields", + "value": "id,name,slug" + }, + { + "key": "ordering", + "value": "name" + } + ], + "paging": { + "mode": "offset", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "250" + }, + "offset": { + "mode": "row", + "base": 0, + "rowCountIn": { + "realm": "payloadArraySize", + "path": "results" + } + }, + "out": { + "realm": "queryArg", + "path": "offset" + } + }, + "postRequestScript": "tags.js" + }, + "matches": "none", + "metadata": [ + { + "name": "label", + "displayName": "Label", + "shape": "string", + "role": "label" + }, + { + "name": "value", + "displayName": "Value", + "shape": "string", + "role": "value" + } + ], + "timeframes": false +} diff --git a/plugins/NetBox/v1/dataStreams/virtualMachines.json b/plugins/NetBox/v1/dataStreams/virtualMachines.json index e95e7540..ac83ba45 100644 --- a/plugins/NetBox/v1/dataStreams/virtualMachines.json +++ b/plugins/NetBox/v1/dataStreams/virtualMachines.json @@ -13,11 +13,19 @@ "getArgs": [ { "key": "fields", - "value": "id,display,display_url,status,site,cluster,device,role,tenant,platform,primary_ip,vcpus,memory,disk,description,created,last_updated,custom_fields" + "value": "id,display,display_url,status,site,cluster,device,role,tenant,platform,primary_ip,vcpus,memory,disk,description,created,last_updated,tags,custom_fields" + }, + { + "key": "id", + "value": "{{ (typeof virtualMachine !== 'undefined' && virtualMachine && virtualMachine.length) ? virtualMachine.map(o => [].concat(o.rawId)[0]) : null }}" }, { "key": "site_id", "value": "{{ (typeof site !== 'undefined' && site && site.length) ? site.map(s => [].concat(s.rawId)[0]) : null }}" + }, + { + "key": "tag", + "value": "{{ (typeof tag !== 'undefined' && tag && tag.length) ? [].concat(tag).flatMap(t => (t && typeof t === 'object') ? [t.value] : String(t).split(',')).map(t => String(t).trim()).filter(Boolean) : null }}" } ], "paging": { @@ -45,6 +53,19 @@ }, "matches": "none", "ui": [ + { + "type": "objects", + "name": "virtualMachine", + "label": "Virtual machine(s) (optional)", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Virtual Machine" + ] + } + } + }, { "type": "objects", "name": "site", @@ -57,6 +78,19 @@ ] } } + }, + { + "type": "autocomplete", + "name": "tag", + "label": "Tag(s) (optional)", + "help": "Returns only objects that have all the selected tags", + "isMulti": true, + "allowCustomValues": true, + "data": { + "source": "dataStream", + "dataStreamName": "tags", + "dataSourceConfig": {} + } } ], "metadata": [ @@ -158,6 +192,16 @@ "displayName": "Last Updated", "shape": "date" }, + { + "pattern": "^tags[.].*", + "visible": false + }, + { + "name": "tags.0.display", + "displayName": "Tags", + "shape": "string", + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + }, { "name": "display_url", "displayName": "Link", diff --git a/plugins/NetBox/v1/dataStreams/vlanGroups.json b/plugins/NetBox/v1/dataStreams/vlanGroups.json index 00bb96f4..bf4a7c73 100644 --- a/plugins/NetBox/v1/dataStreams/vlanGroups.json +++ b/plugins/NetBox/v1/dataStreams/vlanGroups.json @@ -13,7 +13,11 @@ "getArgs": [ { "key": "fields", - "value": "id,display,display_url,name,slug,scope_type,scope,tenant,vid_ranges,total_vlan_ids,vlan_count,utilization,description,last_updated,custom_fields" + "value": "id,display,display_url,name,slug,scope_type,scope,tenant,vid_ranges,total_vlan_ids,vlan_count,utilization,description,last_updated,tags,custom_fields" + }, + { + "key": "tag", + "value": "{{ (typeof tag !== 'undefined' && tag && tag.length) ? [].concat(tag).flatMap(t => (t && typeof t === 'object') ? [t.value] : String(t).split(',')).map(t => String(t).trim()).filter(Boolean) : null }}" } ], "paging": { @@ -50,6 +54,21 @@ ] } }, + "ui": [ + { + "type": "autocomplete", + "name": "tag", + "label": "Tag(s) (optional)", + "help": "Returns only objects that have all the selected tags", + "isMulti": true, + "allowCustomValues": true, + "data": { + "source": "dataStream", + "dataStreamName": "tags", + "dataSourceConfig": {} + } + } + ], "metadata": [ { "name": "id", @@ -129,6 +148,16 @@ "displayName": "Last Updated", "shape": "date" }, + { + "pattern": "^tags[.].*", + "visible": false + }, + { + "name": "tags.0.display", + "displayName": "Tags", + "shape": "string", + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + }, { "name": "display_url", "displayName": "Link", diff --git a/plugins/NetBox/v1/docs/README.md b/plugins/NetBox/v1/docs/README.md index 1b52d67e..ce4f5d09 100644 --- a/plugins/NetBox/v1/docs/README.md +++ b/plugins/NetBox/v1/docs/README.md @@ -45,6 +45,7 @@ The plugin works with **NetBox Community**, **NetBox Enterprise**, and **NetBox - **Lifecycle**: devices and virtual machines that aren't active (planned, staged, offline, decommissioning), and circuit termination dates. - **Change activity**: the NetBox change log by user, action and object type over time. - **Custom fields**: every custom field on sites, racks, devices, virtual machines, prefixes, circuits, IP addresses and power feeds appears as an extra column. +- **Tags**: NetBox tags appear as a **Tags** column, can filter list streams, and are stored on imported objects so Collections and scopes can match them. **Dashboards.** The out-of-the-box dashboards are **Overview**, **IPAM Capacity**, **Rack Capacity**, **Data Quality**, **Lifecycle and Circuits**, **Change Activity**, **Data Centers** (including a map of sites), **Power**, and **Device Inventory**. There is also a perspective dashboard for each **Site**, **Rack**, **Device**, **Virtual Machine**, **Prefix** and **Circuit**. @@ -76,6 +77,10 @@ The plugin works with **NetBox Community**, **NetBox Enterprise**, and **NetBox - Build and test queries in NetBox's GraphiQL explorer at `https:///graphql/`. - Add `pagination: { limit: 1000, offset: 0 }` to lists that may be longer than 1,000 items. +**Tags.** Sites, Racks, Devices, Virtual Machines, Prefixes, IP Addresses, VLAN Groups, Circuits and Power Feeds have a **Tags** column (tag names, comma-separated) and an optional **Tag(s)** filter. When several tags are selected, only objects that have all of them are returned. + +**Selected objects and Collections.** Sites, Racks, Devices, Virtual Machines, Prefixes and Circuits also have an optional object filter (**Site(s)**, **Rack(s)**, **Device(s)**, **Virtual machine(s)**, **Prefix(es)**, **Circuit(s)**). It returns only the selected objects, read live from NetBox, including their custom fields and tags. To show a whole Collection, add a dashboard variable based on the Collection, allow multiple selection, default it to all objects, and bind the filter to that variable. + ## What gets indexed | Object type | API source | Represents | @@ -89,6 +94,8 @@ The plugin works with **NetBox Community**, **NetBox Enterprise**, and **NetBox Each object is identified by its NetBox ID and keeps key properties such as status, site, role and tenant. Each also keeps a link back to the object in NetBox. +Each object also keeps its NetBox tags in the **netboxTags** property, as comma-separated tag names (for example `Core, Production`). To build a Collection from one tag, search the objects with `netboxTags:""`, for example `netboxTags:"Production"`. Then bind that Collection to the matching list stream's object filter (see **Selected objects and Collections** above) to see the live custom fields of every object in it. + Imports run every 12 hours by default. Dashboard tiles query NetBox live, but SquaredUp caches results briefly, so use the dashboard's refresh button to see a change straight away. ## Known limitations @@ -110,6 +117,13 @@ Imports run every 12 hours by default. Dashboard tiles query NetBox live, but Sq - **Site map.** - Sites appear on the Data Centers map only if their latitude and longitude are set in NetBox. NetBox doesn't convert addresses into coordinates. - The map uses SmartViz Geo Map. Until it's available in your SquaredUp region, the tile shows a table of sites instead. +- **Tags on imported objects.** + - **netboxTags** holds all of an object's tags as one value, so the **NetboxTags** filter in the Collection editor lists tag combinations rather than single tags. Use the `netboxTags:""` search to select one tag. + - Tag changes appear on objects after the next import. The **Tags** column is always live. +- **Custom fields.** + - They aren't indexed, so they can't be used to build Collections or scopes. Use tags to group objects, and the object filters to show custom fields for a Collection. + - Select, multi-select and object custom fields appear as several columns (for example the value and the label, or the object's ID, name and link). Hide the ones you don't need in the tile. +- **Very large object selections.** The selected objects are sent to NetBox in one request, which NetBox (or a proxy in front of it) may reject as too long when many hundreds of objects are selected at once. Split very large Collections across tiles if that happens. - **Change log.** Available for up to 30 days at a time. On busy instances, filter by object, action or object type. - **Reconnecting after plugin updates.** If a plugin update changes how SquaredUp connects to NetBox, open the data source and save it again. - **NetBox version.** diff --git a/plugins/NetBox/v1/indexDefinitions/default.json b/plugins/NetBox/v1/indexDefinitions/default.json index 2f5fd2b2..17f3464a 100644 --- a/plugins/NetBox/v1/indexDefinitions/default.json +++ b/plugins/NetBox/v1/indexDefinitions/default.json @@ -31,6 +31,9 @@ { "timeZone": "time_zone" }, + { + "netboxTags": "tags.0.display" + }, { "netboxUrl": "display_url" } @@ -71,6 +74,9 @@ { "uHeight": "u_height" }, + { + "netboxTags": "tags.0.display" + }, { "netboxUrl": "display_url" } @@ -132,6 +138,9 @@ { "cluster": "cluster.name" }, + { + "netboxTags": "tags.0.display" + }, { "netboxUrl": "display_url" } @@ -175,6 +184,9 @@ { "primaryIp": "primary_ip.display" }, + { + "netboxTags": "tags.0.display" + }, { "netboxUrl": "display_url" } @@ -218,6 +230,9 @@ { "markUtilized": "mark_utilized" }, + { + "netboxTags": "tags.0.display" + }, { "netboxUrl": "display_url" } @@ -255,6 +270,9 @@ { "terminationDate": "termination_date" }, + { + "netboxTags": "tags.0.display" + }, { "netboxUrl": "display_url" } diff --git a/plugins/NetBox/v1/metadata.json b/plugins/NetBox/v1/metadata.json index 921888b8..0d05f51c 100644 --- a/plugins/NetBox/v1/metadata.json +++ b/plugins/NetBox/v1/metadata.json @@ -1,7 +1,7 @@ { "name": "netbox", "displayName": "NetBox", - "version": "1.0.0", + "version": "1.1.0", "author": { "name": "@shawn149", "type": "community" From d3665e51af8e3336bdbd0892aba896cf64cdaeb8 Mon Sep 17 00:00:00 2001 From: Shawn Williams Date: Tue, 20 Oct 2026 10:59:12 -0400 Subject: [PATCH 2/2] Expose NetBox tags as arrays Update tag columns across NetBox streams to return arrays, enabling tags to be handled individually in Collections. Update the plugin documentation to reflect the new tag behavior. --- plugins/NetBox/v1/dataStreams/circuits.json | 9 +++++++-- plugins/NetBox/v1/dataStreams/devices.json | 9 +++++++-- plugins/NetBox/v1/dataStreams/ipAddresses.json | 9 +++++++-- plugins/NetBox/v1/dataStreams/powerFeeds.json | 9 +++++++-- plugins/NetBox/v1/dataStreams/prefixes.json | 9 +++++++-- plugins/NetBox/v1/dataStreams/racks.json | 9 +++++++-- plugins/NetBox/v1/dataStreams/sites.json | 9 +++++++-- plugins/NetBox/v1/dataStreams/virtualMachines.json | 9 +++++++-- plugins/NetBox/v1/dataStreams/vlanGroups.json | 9 +++++++-- plugins/NetBox/v1/docs/README.md | 8 +++----- 10 files changed, 66 insertions(+), 23 deletions(-) diff --git a/plugins/NetBox/v1/dataStreams/circuits.json b/plugins/NetBox/v1/dataStreams/circuits.json index 2b3b9ef9..fbd5b50b 100644 --- a/plugins/NetBox/v1/dataStreams/circuits.json +++ b/plugins/NetBox/v1/dataStreams/circuits.json @@ -170,8 +170,13 @@ { "name": "tags.0.display", "displayName": "Tags", - "shape": "string", - "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + "shape": [ + "array", + { + "itemShape": "string" + } + ], + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? JSON.stringify(out) : null; })() }}" }, { "name": "display_url", diff --git a/plugins/NetBox/v1/dataStreams/devices.json b/plugins/NetBox/v1/dataStreams/devices.json index 3885ffe5..27727389 100644 --- a/plugins/NetBox/v1/dataStreams/devices.json +++ b/plugins/NetBox/v1/dataStreams/devices.json @@ -301,8 +301,13 @@ { "name": "tags.0.display", "displayName": "Tags", - "shape": "string", - "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + "shape": [ + "array", + { + "itemShape": "string" + } + ], + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? JSON.stringify(out) : null; })() }}" }, { "name": "display_url", diff --git a/plugins/NetBox/v1/dataStreams/ipAddresses.json b/plugins/NetBox/v1/dataStreams/ipAddresses.json index 4faff3d2..754da6d3 100644 --- a/plugins/NetBox/v1/dataStreams/ipAddresses.json +++ b/plugins/NetBox/v1/dataStreams/ipAddresses.json @@ -282,8 +282,13 @@ { "name": "tags.0.display", "displayName": "Tags", - "shape": "string", - "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + "shape": [ + "array", + { + "itemShape": "string" + } + ], + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? JSON.stringify(out) : null; })() }}" }, { "name": "display_url", diff --git a/plugins/NetBox/v1/dataStreams/powerFeeds.json b/plugins/NetBox/v1/dataStreams/powerFeeds.json index fc6dc73d..28209200 100644 --- a/plugins/NetBox/v1/dataStreams/powerFeeds.json +++ b/plugins/NetBox/v1/dataStreams/powerFeeds.json @@ -212,8 +212,13 @@ { "name": "tags.0.display", "displayName": "Tags", - "shape": "string", - "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + "shape": [ + "array", + { + "itemShape": "string" + } + ], + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? JSON.stringify(out) : null; })() }}" }, { "name": "rack.id", diff --git a/plugins/NetBox/v1/dataStreams/prefixes.json b/plugins/NetBox/v1/dataStreams/prefixes.json index 49b9c80c..fa36bd56 100644 --- a/plugins/NetBox/v1/dataStreams/prefixes.json +++ b/plugins/NetBox/v1/dataStreams/prefixes.json @@ -193,8 +193,13 @@ { "name": "tags.0.display", "displayName": "Tags", - "shape": "string", - "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + "shape": [ + "array", + { + "itemShape": "string" + } + ], + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? JSON.stringify(out) : null; })() }}" }, { "name": "display_url", diff --git a/plugins/NetBox/v1/dataStreams/racks.json b/plugins/NetBox/v1/dataStreams/racks.json index 618b13f8..02f29c76 100644 --- a/plugins/NetBox/v1/dataStreams/racks.json +++ b/plugins/NetBox/v1/dataStreams/racks.json @@ -199,8 +199,13 @@ { "name": "tags.0.display", "displayName": "Tags", - "shape": "string", - "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + "shape": [ + "array", + { + "itemShape": "string" + } + ], + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? JSON.stringify(out) : null; })() }}" }, { "name": "display_url", diff --git a/plugins/NetBox/v1/dataStreams/sites.json b/plugins/NetBox/v1/dataStreams/sites.json index c661ce53..dc7cda51 100644 --- a/plugins/NetBox/v1/dataStreams/sites.json +++ b/plugins/NetBox/v1/dataStreams/sites.json @@ -188,8 +188,13 @@ { "name": "tags.0.display", "displayName": "Tags", - "shape": "string", - "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + "shape": [ + "array", + { + "itemShape": "string" + } + ], + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? JSON.stringify(out) : null; })() }}" }, { "name": "display_url", diff --git a/plugins/NetBox/v1/dataStreams/virtualMachines.json b/plugins/NetBox/v1/dataStreams/virtualMachines.json index ac83ba45..f6dfbb6f 100644 --- a/plugins/NetBox/v1/dataStreams/virtualMachines.json +++ b/plugins/NetBox/v1/dataStreams/virtualMachines.json @@ -199,8 +199,13 @@ { "name": "tags.0.display", "displayName": "Tags", - "shape": "string", - "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + "shape": [ + "array", + { + "itemShape": "string" + } + ], + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? JSON.stringify(out) : null; })() }}" }, { "name": "display_url", diff --git a/plugins/NetBox/v1/dataStreams/vlanGroups.json b/plugins/NetBox/v1/dataStreams/vlanGroups.json index bf4a7c73..1d77334e 100644 --- a/plugins/NetBox/v1/dataStreams/vlanGroups.json +++ b/plugins/NetBox/v1/dataStreams/vlanGroups.json @@ -155,8 +155,13 @@ { "name": "tags.0.display", "displayName": "Tags", - "shape": "string", - "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? out.join(', ') : null; })() }}" + "shape": [ + "array", + { + "itemShape": "string" + } + ], + "valueExpression": "{{ (() => { const out = []; for (let i = 0; i < 100; i++) { const n = $['tags.' + i + '.name']; if (typeof n !== 'string' || !n) break; out.push(n); } return out.length ? JSON.stringify(out) : null; })() }}" }, { "name": "display_url", diff --git a/plugins/NetBox/v1/docs/README.md b/plugins/NetBox/v1/docs/README.md index ce4f5d09..6a852ee1 100644 --- a/plugins/NetBox/v1/docs/README.md +++ b/plugins/NetBox/v1/docs/README.md @@ -77,7 +77,7 @@ The plugin works with **NetBox Community**, **NetBox Enterprise**, and **NetBox - Build and test queries in NetBox's GraphiQL explorer at `https:///graphql/`. - Add `pagination: { limit: 1000, offset: 0 }` to lists that may be longer than 1,000 items. -**Tags.** Sites, Racks, Devices, Virtual Machines, Prefixes, IP Addresses, VLAN Groups, Circuits and Power Feeds have a **Tags** column (tag names, comma-separated) and an optional **Tag(s)** filter. When several tags are selected, only objects that have all of them are returned. +**Tags.** Sites, Racks, Devices, Virtual Machines, Prefixes, IP Addresses, VLAN Groups, Circuits and Power Feeds have a **Tags** column and an optional **Tag(s)** filter. When several tags are selected, only objects that have all of them are returned. **Selected objects and Collections.** Sites, Racks, Devices, Virtual Machines, Prefixes and Circuits also have an optional object filter (**Site(s)**, **Rack(s)**, **Device(s)**, **Virtual machine(s)**, **Prefix(es)**, **Circuit(s)**). It returns only the selected objects, read live from NetBox, including their custom fields and tags. To show a whole Collection, add a dashboard variable based on the Collection, allow multiple selection, default it to all objects, and bind the filter to that variable. @@ -94,7 +94,7 @@ The plugin works with **NetBox Community**, **NetBox Enterprise**, and **NetBox Each object is identified by its NetBox ID and keeps key properties such as status, site, role and tenant. Each also keeps a link back to the object in NetBox. -Each object also keeps its NetBox tags in the **netboxTags** property, as comma-separated tag names (for example `Core, Production`). To build a Collection from one tag, search the objects with `netboxTags:""`, for example `netboxTags:"Production"`. Then bind that Collection to the matching list stream's object filter (see **Selected objects and Collections** above) to see the live custom fields of every object in it. +Each object also keeps its NetBox tags in the **netboxTags** property, with one value per tag. To build a Collection from a tag, filter the objects on **NetboxTags** and pick the tag. Then bind that Collection to the matching list stream's object filter (see **Selected objects and Collections** above) to see the live custom fields of every object in it. Imports run every 12 hours by default. Dashboard tiles query NetBox live, but SquaredUp caches results briefly, so use the dashboard's refresh button to see a change straight away. @@ -117,9 +117,7 @@ Imports run every 12 hours by default. Dashboard tiles query NetBox live, but Sq - **Site map.** - Sites appear on the Data Centers map only if their latitude and longitude are set in NetBox. NetBox doesn't convert addresses into coordinates. - The map uses SmartViz Geo Map. Until it's available in your SquaredUp region, the tile shows a table of sites instead. -- **Tags on imported objects.** - - **netboxTags** holds all of an object's tags as one value, so the **NetboxTags** filter in the Collection editor lists tag combinations rather than single tags. Use the `netboxTags:""` search to select one tag. - - Tag changes appear on objects after the next import. The **Tags** column is always live. +- **Tags on imported objects.** Tag changes appear on objects after the next import. The **Tags** column is always live. - **Custom fields.** - They aren't indexed, so they can't be used to build Collections or scopes. Use tags to group objects, and the object filters to show custom fields for a Collection. - Select, multi-select and object custom fields appear as several columns (for example the value and the label, or the object's ID, name and link). Hide the ones you don't need in the tile.