diff --git a/plugins/N-Central/v1/configValidation.json b/plugins/N-Central/v1/configValidation.json new file mode 100644 index 00000000..73c7de5a --- /dev/null +++ b/plugins/N-Central/v1/configValidation.json @@ -0,0 +1,11 @@ +{ + "steps": [ + { + "displayName": "Authenticate", + "dataStream": { "name": "serverInfoAuthenticated" }, + "required": true, + "error": "Could not authenticate with N-central. Check the Server URL and User-API Token are correct and that the token hasn't expired.", + "success": "Connected successfully." + } + ] +} diff --git a/plugins/N-Central/v1/custom_types.json b/plugins/N-Central/v1/custom_types.json new file mode 100644 index 00000000..aa6df796 --- /dev/null +++ b/plugins/N-Central/v1/custom_types.json @@ -0,0 +1,30 @@ +[ + { + "name": "Service Organization", + "sourceType": "Service Organization", + "icon": "building", + "singular": "Service Organization", + "plural": "Service Organizations" + }, + { + "name": "Customer", + "sourceType": "Customer", + "icon": "briefcase", + "singular": "Customer", + "plural": "Customers" + }, + { + "name": "Site", + "sourceType": "Site", + "icon": "location-dot", + "singular": "Site", + "plural": "Sites" + }, + { + "name": "Device", + "sourceType": "Device", + "icon": "microchip", + "singular": "Device", + "plural": "Devices" + } +] diff --git a/plugins/N-Central/v1/dataStreams/customerActiveIssues.json b/plugins/N-Central/v1/dataStreams/customerActiveIssues.json new file mode 100644 index 00000000..c21b47b4 --- /dev/null +++ b/plugins/N-Central/v1/dataStreams/customerActiveIssues.json @@ -0,0 +1,58 @@ +{ + "name": "customerActiveIssues", + "displayName": "Customer Active Issues", + "description": "Currently active issues and alerts for a customer", + "tags": ["Customer", "Alerts"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "api/org-units/{{object.rawId}}/active-issues", + "pathToData": "data", + "expandInnerObjects": true, + "paging": { + "mode": "offset", + "pageSize": { "realm": "queryArg", "path": "pageSize", "value": "1000" }, + "offset": { + "mode": "page", + "rowCountIn": { "realm": "payloadArraySize", "path": "data" }, + "base": 1 + }, + "out": { "realm": "queryArg", "path": "pageNumber" } + } + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Customer"] } }, + "metadata": [ + { "name": "orgUnitId", "shape": "number", "visible": false }, + { "name": "deviceId", "shape": "number", "visible": false }, + { + "name": "_extra.deviceName", + "displayName": "Device", + "shape": "string", + "role": "label", + "sourceId": "deviceId", + "sourceType": "Device" + }, + { "name": "serviceName", "displayName": "Service", "shape": "string" }, + { "name": "serviceType", "displayName": "Service Type", "shape": "string" }, + { "name": "notificationState", "displayName": "Notification State", "shape": "number" }, + { + "name": "_extra.transitionTime", + "displayName": "Since", + "shape": "date", + "role": "timestamp" + }, + { "name": "_extra.licenseMode", "displayName": "License Mode", "shape": "string" }, + { + "name": "_extra.remoteControllable", + "displayName": "Remote Controllable", + "shape": "boolean" + }, + { "name": "serviceId", "shape": "number", "visible": false }, + { "name": "taskId", "shape": "number", "visible": false }, + { "name": "serviceItemId", "shape": "number", "visible": false }, + { "name": "_extra.customerTree.0", "shape": "string", "visible": false }, + { "name": "_extra.customerTree.1", "shape": "string", "visible": false }, + { "name": "_extra.customerTree.2", "shape": "string", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/N-Central/v1/dataStreams/customers.json b/plugins/N-Central/v1/dataStreams/customers.json new file mode 100644 index 00000000..eec120dd --- /dev/null +++ b/plugins/N-Central/v1/dataStreams/customers.json @@ -0,0 +1,26 @@ +{ + "name": "customers", + "displayName": "Customers", + "description": "Customers, one row per customer", + "tags": ["Organization"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "api/customers", + "pathToData": "data", + "paging": { + "mode": "offset", + "pageSize": { "realm": "queryArg", "path": "pageSize", "value": "200" }, + "offset": { + "mode": "page", + "rowCountIn": { "realm": "payloadArraySize", "path": "data" }, + "base": 1 + }, + "out": { "realm": "queryArg", "path": "pageNumber" } + } + }, + "matches": "none", + "metadata": [{ "pattern": ".*" }], + "timeframes": false, + "visibility": { "type": "hidden" } +} diff --git a/plugins/N-Central/v1/dataStreams/deviceAssets.json b/plugins/N-Central/v1/dataStreams/deviceAssets.json new file mode 100644 index 00000000..b97b8c89 --- /dev/null +++ b/plugins/N-Central/v1/dataStreams/deviceAssets.json @@ -0,0 +1,20 @@ +{ + "name": "deviceAssets", + "displayName": "Device Assets", + "description": "Hardware and software inventory for a device", + "tags": ["Device", "Inventory"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "api/devices/{{object.rawId}}/assets", + "postRequestScript": "deviceAssets.js" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Device"] } }, + "metadata": [ + { "name": "category", "displayName": "Category", "shape": "string" }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "id", "displayName": "ID", "shape": "string", "role": "id", "visible": false }, + { "pattern": ".*" } + ], + "timeframes": false +} diff --git a/plugins/N-Central/v1/dataStreams/deviceCount.json b/plugins/N-Central/v1/dataStreams/deviceCount.json new file mode 100644 index 00000000..d462d0f9 --- /dev/null +++ b/plugins/N-Central/v1/dataStreams/deviceCount.json @@ -0,0 +1,17 @@ +{ + "name": "deviceCount", + "displayName": "Device Count", + "description": "Total number of managed devices, read from the API's own pagination total rather than counting individual objects", + "tags": ["Device"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "api/devices", + "getArgs": [{ "key": "pageSize", "value": "1" }], + "pathToData": "totalItems", + "paging": { "mode": "none" } + }, + "matches": "none", + "metadata": [{ "name": "result", "displayName": "Total Devices", "shape": "number" }], + "timeframes": false +} diff --git a/plugins/N-Central/v1/dataStreams/deviceLifecycleInfo.json b/plugins/N-Central/v1/dataStreams/deviceLifecycleInfo.json new file mode 100644 index 00000000..148e7eae --- /dev/null +++ b/plugins/N-Central/v1/dataStreams/deviceLifecycleInfo.json @@ -0,0 +1,25 @@ +{ + "name": "deviceLifecycleInfo", + "displayName": "Device Lifecycle Info", + "description": "Warranty, lease, and hardware replacement details for a device", + "tags": ["Device", "Inventory"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "api/devices/{{object.rawId}}/assets/lifecycle-info" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Device"] } }, + "metadata": [ + { "name": "warrantyExpiryDate", "displayName": "Warranty Expiry Date", "shape": "date" }, + { "name": "leaseExpiryDate", "displayName": "Lease Expiry Date", "shape": "date" }, + { "name": "expectedReplacementDate", "displayName": "Expected Replacement Date", "shape": "date" }, + { "name": "purchaseDate", "displayName": "Purchase Date", "shape": "date" }, + { "name": "cost", "displayName": "Cost", "shape": ["number", { "decimalPlaces": 2 }] }, + { "name": "location", "displayName": "Location", "shape": "string" }, + { "name": "assetTag", "displayName": "Asset Tag", "shape": "string" }, + { "name": "description", "displayName": "Description", "shape": "string" }, + { "name": "updateWarrantyError", "displayName": "Update Warranty Error", "shape": "string" }, + { "pattern": ".*" } + ], + "timeframes": false +} diff --git a/plugins/N-Central/v1/dataStreams/deviceList.json b/plugins/N-Central/v1/dataStreams/deviceList.json new file mode 100644 index 00000000..f4a2e38e --- /dev/null +++ b/plugins/N-Central/v1/dataStreams/deviceList.json @@ -0,0 +1,53 @@ +{ + "name": "deviceList", + "displayName": "Devices", + "description": "Managed devices, optionally filtered to a customer or site, one row per device", + "tags": ["Customer", "Site", "Device"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "{{ (typeof orgUnit !== 'undefined' && orgUnit && orgUnit.length) ? 'api/org-units/' + (Array.isArray(orgUnit[0].rawId) ? orgUnit[0].rawId[0] : orgUnit[0].rawId) + '/devices' : 'api/devices' }}", + "pathToData": "data", + "paging": { + "mode": "offset", + "pageSize": { "realm": "queryArg", "path": "pageSize", "value": "1000" }, + "offset": { + "mode": "page", + "rowCountIn": { "realm": "payloadArraySize", "path": "data" }, + "base": 1 + }, + "out": { "realm": "queryArg", "path": "pageNumber" } + } + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "orgUnit", + "label": "Customer or Site (optional)", + "objectLimit": 1, + "matches": { + "sourceType": { "type": "oneOf", "values": ["Customer", "Site"] } + } + } + ], + "metadata": [ + { + "name": "longName", + "displayName": "Device", + "shape": "string", + "role": "label", + "sourceId": "deviceId", + "sourceType": "Device" + }, + { "name": "deviceId", "shape": "number", "visible": false }, + { "name": "deviceClass", "displayName": "Device Class", "shape": "string" }, + { "name": "supportedOs", "displayName": "OS", "shape": "string" }, + { "name": "isProbe", "displayName": "Probe", "shape": "boolean" }, + { "name": "licenseMode", "displayName": "License Mode", "shape": "string" }, + { "name": "customerId", "shape": "number", "visible": false }, + { "name": "siteId", "shape": "number", "visible": false }, + { "pattern": ".*" } + ], + "timeframes": false +} diff --git a/plugins/N-Central/v1/dataStreams/deviceServiceMonitorStatus.json b/plugins/N-Central/v1/dataStreams/deviceServiceMonitorStatus.json new file mode 100644 index 00000000..79b782eb --- /dev/null +++ b/plugins/N-Central/v1/dataStreams/deviceServiceMonitorStatus.json @@ -0,0 +1,51 @@ +{ + "name": "deviceServiceMonitorStatus", + "displayName": "Device Service Monitor Status", + "description": "Current service monitor health status for a device", + "tags": ["Device", "Monitoring"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "api/devices/{{object.rawId}}/service-monitor-status", + "pathToData": "data" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Device"] } }, + "metadata": [ + { "name": "moduleName", "displayName": "Service", "shape": "string", "role": "label" }, + { "name": "taskIdent", "displayName": "Instance", "shape": "string" }, + { + "name": "stateStatus", + "displayName": "Status", + "shape": [ + "state", + { + "map": { + "success": ["Normal"], + "warning": ["Misconfigured", "Stale", "Warning"], + "error": ["Failed"], + "unmonitored": ["Disabled", "Unmanaged"] + } + } + ] + }, + { "name": "taskNote", "displayName": "Template", "shape": "string" }, + { + "name": "lastScanTime", + "displayName": "Last Scan", + "shape": ["date", { "inputPattern": "yyyy-MM-dd HH:mm:ss.SSS xx" }] + }, + { + "name": "transitionTime", + "displayName": "Status Changed", + "shape": ["date", { "inputPattern": "yyyy-MM-dd HH:mm:ss.SSS xx" }] + }, + { "name": "timeToStale", "displayName": "Time to Stale (s)", "shape": "seconds" }, + { "name": "applianceName", "displayName": "Probe", "shape": "string" }, + { "name": "isManagedTask", "displayName": "Managed", "shape": "boolean" }, + { "name": "taskId", "displayName": "Task ID", "shape": "number", "role": "id", "visible": false }, + { "name": "serviceId", "displayName": "Service ID", "shape": "number", "visible": false }, + { "name": "serviceItemId", "displayName": "Service Item ID", "shape": "number", "visible": false }, + { "name": "applianceId", "displayName": "Appliance ID", "shape": "string", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/N-Central/v1/dataStreams/devices.json b/plugins/N-Central/v1/dataStreams/devices.json new file mode 100644 index 00000000..d8d8f767 --- /dev/null +++ b/plugins/N-Central/v1/dataStreams/devices.json @@ -0,0 +1,26 @@ +{ + "name": "devices", + "displayName": "Devices", + "description": "Managed devices, one row per device", + "tags": ["Device"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "api/devices", + "pathToData": "data", + "paging": { + "mode": "offset", + "pageSize": { "realm": "queryArg", "path": "pageSize", "value": "1000" }, + "offset": { + "mode": "page", + "rowCountIn": { "realm": "payloadArraySize", "path": "data" }, + "base": 1 + }, + "out": { "realm": "queryArg", "path": "pageNumber" } + } + }, + "matches": "none", + "metadata": [{ "pattern": ".*" }], + "timeframes": false, + "visibility": { "type": "hidden" } +} diff --git a/plugins/N-Central/v1/dataStreams/scripts/deviceAssets.js b/plugins/N-Central/v1/dataStreams/scripts/deviceAssets.js new file mode 100644 index 00000000..439ca5e0 --- /dev/null +++ b/plugins/N-Central/v1/dataStreams/scripts/deviceAssets.js @@ -0,0 +1,124 @@ +// dataStreams/scripts/deviceAssets.js +// +// GET api/devices/{deviceId}/assets returns a bag of asset categories, each +// either a singleton object (e.g. "os", "processor") or a { list: [...] } +// array (e.g. "application", "memory"). A near-duplicate set of the same +// category names is repeated under "_extra" with a different (usually +// complementary, occasionally overlapping) set of fields for the same +// entities. Flatten everything into one row per discrete asset item, tagged +// with which category it came from. +const body = (data && data.data) || {}; +const extra = body._extra || {}; + +// Organizational/hierarchy context, not device asset info - already covered +// by the customers/sites data streams. +const skipCategories = new Set(["customer", "site", "socustomer"]); + +function nameFor(category, item) { + return ( + item.displayname || + item.caption || + item.name || + item.description || + item.title || + item.model || + item.modelnumber || + item.volumename || + item.servicename || + item.longname || + item.netbiosname || + item.reportedos || + item.product || + item.sharename || + item.partnumber || + item.uniqueid || + category + ); +} + +const rows = []; +const counters = {}; + +function addItem(category, item) { + if (!item) { + return; + } + const { _index, ...details } = item; + const i = (counters[category] = (counters[category] || 0) + 1); + // Spread the raw fields first, then set our own columns last so a raw + // field that happens to share a name (e.g. "patch" items have their own + // "category" field, a UUID) can never clobber the synthetic ones below. + rows.push({ + ...details, + category, + name: nameFor(category, item), + id: `${category}-${_index ?? i}`, + }); +} + +// Singleton categories describe the device itself; top-level and "_extra" +// hold different (non-overlapping) fields for the same entity, so merge them +// into a single row per category instead of emitting two. +["device", "computersystem", "os", "processor", "motherboard"].forEach( + (key) => { + const merged = Object.assign({}, body[key] || {}, extra[key] || {}); + if (Object.keys(merged).length > 0) { + addItem(key, merged); + } + }, +); + +// List categories: top-level and "_extra" entries sharing the same "_index" +// describe the same underlying asset with complementary (occasionally +// overlapping) fields, so merge them by "_index" the same way singleton +// categories are merged above. "_extra" fields win on conflicts. Entries with +// no matching "_index" on the other side are kept as-is. +const listKeys = new Set([ + ...Object.keys(body).filter( + (k) => k !== "_extra" && body[k] && Array.isArray(body[k].list), + ), + ...Object.keys(extra).filter( + (k) => extra[k] && Array.isArray(extra[k].list), + ), +]); + +listKeys.forEach((key) => { + if (skipCategories.has(key)) { + return; + } + const bodyList = (body[key] && body[key].list) || []; + const extraList = (extra[key] && extra[key].list) || []; + + const byIndex = new Map(); + const unindexed = []; + + bodyList.forEach((item) => { + if (item && item._index != null) { + byIndex.set(item._index, item); + } else { + unindexed.push(item); + } + }); + + extraList.forEach((item) => { + if (!item) { + return; + } + if (item._index != null && byIndex.has(item._index)) { + byIndex.set(item._index, { + ...byIndex.get(item._index), + ...item, + }); + } else if (item._index != null) { + byIndex.set(item._index, item); + } else { + unindexed.push(item); + } + }); + + [...byIndex.values(), ...unindexed].forEach((item) => + addItem(key, item), + ); +}); + +result = rows; diff --git a/plugins/N-Central/v1/dataStreams/serverInfoAuthenticated.json b/plugins/N-Central/v1/dataStreams/serverInfoAuthenticated.json new file mode 100644 index 00000000..b66447af --- /dev/null +++ b/plugins/N-Central/v1/dataStreams/serverInfoAuthenticated.json @@ -0,0 +1,19 @@ +{ + "name": "serverInfoAuthenticated", + "displayName": "Service Organizations Probe", + "description": "Minimal authenticated call to confirm the User-API Token works", + "tags": ["System"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "api/service-orgs", + "getArgs": [ + { "key": "pageSize", "value": "1" }, + { "key": "pageNumber", "value": "1" } + ] + }, + "matches": "none", + "metadata": [{ "pattern": ".*" }], + "timeframes": false, + "visibility": { "type": "hidden" } +} diff --git a/plugins/N-Central/v1/dataStreams/serviceOrgs.json b/plugins/N-Central/v1/dataStreams/serviceOrgs.json new file mode 100644 index 00000000..234fb51a --- /dev/null +++ b/plugins/N-Central/v1/dataStreams/serviceOrgs.json @@ -0,0 +1,26 @@ +{ + "name": "serviceOrgs", + "displayName": "Service Organizations", + "description": "Service organizations, one row per organization", + "tags": ["Organization"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "api/service-orgs", + "pathToData": "data", + "paging": { + "mode": "offset", + "pageSize": { "realm": "queryArg", "path": "pageSize", "value": "200" }, + "offset": { + "mode": "page", + "rowCountIn": { "realm": "payloadArraySize", "path": "data" }, + "base": 1 + }, + "out": { "realm": "queryArg", "path": "pageNumber" } + } + }, + "matches": "none", + "metadata": [{ "pattern": ".*" }], + "timeframes": false, + "visibility": { "type": "hidden" } +} diff --git a/plugins/N-Central/v1/dataStreams/sites.json b/plugins/N-Central/v1/dataStreams/sites.json new file mode 100644 index 00000000..806ba9f8 --- /dev/null +++ b/plugins/N-Central/v1/dataStreams/sites.json @@ -0,0 +1,26 @@ +{ + "name": "sites", + "displayName": "Sites", + "description": "Sites, one row per site", + "tags": ["Organization"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "api/sites", + "pathToData": "data", + "paging": { + "mode": "offset", + "pageSize": { "realm": "queryArg", "path": "pageSize", "value": "200" }, + "offset": { + "mode": "page", + "rowCountIn": { "realm": "payloadArraySize", "path": "data" }, + "base": 1 + }, + "out": { "realm": "queryArg", "path": "pageNumber" } + } + }, + "matches": "none", + "metadata": [{ "pattern": ".*" }], + "timeframes": false, + "visibility": { "type": "hidden" } +} diff --git a/plugins/N-Central/v1/dataStreams/soCustomers.json b/plugins/N-Central/v1/dataStreams/soCustomers.json new file mode 100644 index 00000000..77e47406 --- /dev/null +++ b/plugins/N-Central/v1/dataStreams/soCustomers.json @@ -0,0 +1,38 @@ +{ + "name": "soCustomers", + "displayName": "Service Org Customers", + "description": "Customers belonging to a service organization, one row per customer", + "tags": ["Organization", "Customer"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "api/customers", + "pathToData": "data", + "getArgs": [{ "key": "select", "value": "parentId=={{object.rawId}}" }], + "paging": { + "mode": "offset", + "pageSize": { "realm": "queryArg", "path": "pageSize", "value": "100" }, + "offset": { + "mode": "page", + "rowCountIn": { "realm": "payloadArraySize", "path": "data" }, + "base": 1 + }, + "out": { "realm": "queryArg", "path": "pageNumber" } + } + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Service Organization"] } }, + "metadata": [ + { + "name": "customerName", + "displayName": "Customer", + "shape": "string", + "role": "label", + "sourceId": "customerId", + "sourceType": "Customer" + }, + { "name": "customerId", "shape": "number", "visible": false }, + { "name": "parentId", "shape": "number", "visible": false }, + { "pattern": ".*" } + ], + "timeframes": false +} diff --git a/plugins/N-Central/v1/defaultContent/customerPerspective.dash.json b/plugins/N-Central/v1/defaultContent/customerPerspective.dash.json new file mode 100644 index 00000000..7afb5ee8 --- /dev/null +++ b/plugins/N-Central/v1/defaultContent/customerPerspective.dash.json @@ -0,0 +1,227 @@ +{ + "name": "Customer", + "schemaVersion": "1.5", + "variables": ["{{variables.[Customer]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "85228a4a-5eb9-49c4-9923-31340511877c", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active Issues", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Customer]}}"], + "dataStream": { + "id": "{{dataStreams.customerActiveIssues}}", + "name": "customerActiveIssues", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Customer]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": { "type": "count" }, + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "c9671910-5472-48be-84cb-5fae864dbc10", + "x": 0, + "y": 2, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Customer Properties", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Customer]}}"], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Customer]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { "transpose": true } + } + } + } + }, + { + "i": "c8a71885-563d-4dde-a720-b2a8141d9ec1", + "x": 2, + "y": 2, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active Issues by Service Type", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Customer]}}"], + "dataStream": { + "id": "{{dataStreams.customerActiveIssues}}", + "name": "customerActiveIssues", + "pluginConfigId": "{{configId}}", + "group": { + "by": [["serviceType", "uniqueValues"]], + "aggregate": [{ "type": "count" }] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Customer]}}" + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "serviceType_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "right", + "legendMode": "table" + } + } + } + } + }, + { + "i": "d3f72f91-e4ae-43ac-b2d9-a5a8ebdf45f5", + "x": 0, + "y": 6, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active Issues", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Customer]}}"], + "dataStream": { + "id": "{{dataStreams.customerActiveIssues}}", + "name": "customerActiveIssues", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [["_extra.transitionTime", "desc"]] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Customer]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "_extra.deviceName", + "serviceName", + "serviceType", + "notificationState", + "_extra.transitionTime" + ], + "hiddenColumns": [ + "orgUnitId", + "deviceId", + "_extra.licenseMode", + "_extra.remoteControllable", + "serviceId", + "taskId", + "serviceItemId" + ] + } + } + } + } + }, + { + "i": "e43578f5-5ee6-430c-8414-7ee18f04b907", + "x": 0, + "y": 10, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Devices", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Customer]}}"], + "dataStream": { + "id": "{{dataStreams.deviceList}}", + "name": "deviceList", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "orgUnit": { + "variable": "{{variables.[Customer]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Customers]}}" + } + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "longName", + "deviceClass", + "supportedOs", + "licenseMode", + "isProbe" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/N-Central/v1/defaultContent/devicePerspective.dash.json b/plugins/N-Central/v1/defaultContent/devicePerspective.dash.json new file mode 100644 index 00000000..1985a538 --- /dev/null +++ b/plugins/N-Central/v1/defaultContent/devicePerspective.dash.json @@ -0,0 +1,271 @@ +{ + "name": "Device", + "schemaVersion": "1.5", + "variables": ["{{variables.[Device]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "05b21c91-f7b4-46b5-b328-c5aa8b930f14", + "x": 0, + "y": 0, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Device Properties", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Device]}}"], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Devices]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Device]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { "transpose": true } + } + } + } + }, + { + "i": "d15c7009-d882-487a-807e-3e8d74976060", + "x": 2, + "y": 0, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Lifecycle Info", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Device]}}"], + "dataStream": { + "id": "{{dataStreams.deviceLifecycleInfo}}", + "name": "deviceLifecycleInfo", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Devices]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Device]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { "transpose": true } + } + } + } + }, + { + "i": "910e234f-ea9d-459a-82d3-9edc4a6e6cca", + "x": 0, + "y": 4, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Service Monitor Health", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Device]}}"], + "dataStream": { + "id": "{{dataStreams.deviceServiceMonitorStatus}}", + "name": "deviceServiceMonitorStatus", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Devices]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Device]}}" + }, + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "moduleName", + "stateColumn": "stateStatus", + "sublabel": "applianceName", + "linkColumn": "none", + "columns": 4 + } + } + } + } + }, + { + "i": "afadff99-f474-416e-864c-2fb785213429", + "x": 0, + "y": 8, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Asset Category Breakdown", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Device]}}"], + "dataStream": { + "id": "{{dataStreams.deviceAssets}}", + "name": "deviceAssets", + "pluginConfigId": "{{configId}}", + "group": { + "by": [["category", "uniqueValues"]], + "aggregate": [{ "type": "count" }] + }, + "sort": { + "by": [["count", "desc"]], + "top": 10 + } + }, + "scope": { + "scope": "{{scopes.[Devices]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Device]}}" + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "category_uniqueValues", + "yAxisData": ["count"], + "xAxisGroup": "none", + "xAxisLabel": "Category", + "yAxisLabel": "Items", + "showXAxisLabel": true, + "showYAxisLabel": true, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "showValue": true, + "showTotals": false, + "displayMode": "actual", + "horizontalLayout": "horizontal", + "grouping": false, + "range": { "type": "auto" } + } + } + } + } + }, + { + "i": "caac587c-745c-4cf6-9f4a-b717debbda19", + "x": 2, + "y": 8, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Service Monitor Status", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Device]}}"], + "dataStream": { + "id": "{{dataStreams.deviceServiceMonitorStatus}}", + "name": "deviceServiceMonitorStatus", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [["moduleName", "asc"]] + } + }, + "scope": { + "scope": "{{scopes.[Devices]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Device]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "moduleName", + "stateStatus", + "lastScanTime", + "applianceName", + "transitionTime" + ], + "hiddenColumns": [ + "taskIdent", + "taskNote", + "timeToStale", + "isManagedTask" + ] + } + } + } + } + }, + { + "i": "5e436233-622a-41e2-af3f-67a8a4f2d9b8", + "x": 0, + "y": 12, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Device Assets", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Device]}}"], + "dataStream": { + "id": "{{dataStreams.deviceAssets}}", + "name": "deviceAssets", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [["category", "asc"]] + } + }, + "scope": { + "scope": "{{scopes.[Devices]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Device]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["category", "name"], + "hiddenColumns": ["id"] + } + } + } + } + } + ] + } +} diff --git a/plugins/N-Central/v1/defaultContent/manifest.json b/plugins/N-Central/v1/defaultContent/manifest.json new file mode 100644 index 00000000..dc3b4d76 --- /dev/null +++ b/plugins/N-Central/v1/defaultContent/manifest.json @@ -0,0 +1,9 @@ +{ + "items": [ + { "name": "overview", "type": "dashboard" }, + { "name": "serviceOrgPerspective", "type": "dashboard" }, + { "name": "customerPerspective", "type": "dashboard" }, + { "name": "sitePerspective", "type": "dashboard" }, + { "name": "devicePerspective", "type": "dashboard" } + ] +} diff --git a/plugins/N-Central/v1/defaultContent/overview.dash.json b/plugins/N-Central/v1/defaultContent/overview.dash.json new file mode 100644 index 00000000..bffc0ca1 --- /dev/null +++ b/plugins/N-Central/v1/defaultContent/overview.dash.json @@ -0,0 +1,277 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "variables": [], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "862fe8ea-54fc-4955-bc46-23c829dbae0a", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Service Organizations", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Service Organizations]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": { "type": "count" }, + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "8d22fe52-00fe-47da-b539-c61b1f4620ac", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Customers", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": { "type": "count" }, + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "d5132339-deb3-416a-a34a-cbea83fa148d", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sites", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Sites]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": { "type": "count" }, + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "b7601da2-ac87-4d47-8d72-3e6f4c1b4547", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Devices", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.deviceCount}}", + "name": "deviceCount", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "result", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "2fbbdba0-add2-4c43-8060-8e1739686151", + "x": 0, + "y": 2, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active Issues", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.customerActiveIssues}}", + "name": "customerActiveIssues", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": { "type": "count" }, + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "d54f1094-36e2-4cb6-b842-385c190a062b", + "x": 0, + "y": 4, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active Issues by Service Type", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.customerActiveIssues}}", + "name": "customerActiveIssues", + "pluginConfigId": "{{configId}}", + "group": { + "by": [["serviceType", "uniqueValues"]], + "aggregate": [{ "type": "count" }] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "serviceType_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "right", + "legendMode": "table" + } + } + } + } + }, + { + "i": "b87c5a1c-079d-40d4-96ad-291f993d32d4", + "x": 0, + "y": 8, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active Issues", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.customerActiveIssues}}", + "name": "customerActiveIssues", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [["_extra.transitionTime", "desc"]], + "top": 15 + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "_extra.deviceName", + "serviceName", + "serviceType", + "notificationState", + "_extra.transitionTime" + ], + "hiddenColumns": [ + "orgUnitId", + "deviceId", + "_extra.licenseMode", + "_extra.remoteControllable", + "serviceId", + "taskId", + "serviceItemId" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/N-Central/v1/defaultContent/scopes.json b/plugins/N-Central/v1/defaultContent/scopes.json new file mode 100644 index 00000000..8814b9df --- /dev/null +++ b/plugins/N-Central/v1/defaultContent/scopes.json @@ -0,0 +1,50 @@ +[ + { + "name": "Service Organizations", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Service Organization"] } + }, + "variable": { + "name": "Service Organization", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Customers", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Customer"] } + }, + "variable": { + "name": "Customer", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Sites", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Site"] } + }, + "variable": { + "name": "Site", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Devices", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Device"] } + }, + "variable": { + "name": "Device", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/N-Central/v1/defaultContent/serviceOrgPerspective.dash.json b/plugins/N-Central/v1/defaultContent/serviceOrgPerspective.dash.json new file mode 100644 index 00000000..16bc5aa6 --- /dev/null +++ b/plugins/N-Central/v1/defaultContent/serviceOrgPerspective.dash.json @@ -0,0 +1,82 @@ +{ + "name": "Service Organization", + "schemaVersion": "1.5", + "variables": ["{{variables.[Service Organization]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "7da4be41-b6c7-472b-b78f-4af73f748244", + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Service Organization Properties", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Service Organization]}}"], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Service Organizations]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service Organization]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { "transpose": true } + } + } + } + }, + { + "i": "f5ac90fd-701d-42e2-bb98-fd5e1894b4c6", + "x": 0, + "y": 4, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Customers", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Service Organization]}}"], + "dataStream": { + "id": "{{dataStreams.soCustomers}}", + "name": "soCustomers", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Service Organizations]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service Organization]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["customerName"] + } + } + } + } + } + ] + } +} diff --git a/plugins/N-Central/v1/defaultContent/sitePerspective.dash.json b/plugins/N-Central/v1/defaultContent/sitePerspective.dash.json new file mode 100644 index 00000000..3580b834 --- /dev/null +++ b/plugins/N-Central/v1/defaultContent/sitePerspective.dash.json @@ -0,0 +1,90 @@ +{ + "name": "Site", + "schemaVersion": "1.5", + "variables": ["{{variables.[Site]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "307a6700-0b53-42a8-883c-01f4af4fd34e", + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Site Properties", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Site]}}"], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Sites]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Site]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { "transpose": true } + } + } + } + }, + { + "i": "4b4f12a0-2002-4b6e-aaf5-f88c651bd765", + "x": 0, + "y": 4, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Devices", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Site]}}"], + "dataStream": { + "id": "{{dataStreams.deviceList}}", + "name": "deviceList", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "orgUnit": { + "variable": "{{variables.[Site]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Sites]}}" + } + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "longName", + "deviceClass", + "supportedOs", + "licenseMode", + "isProbe" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/N-Central/v1/docs/README.md b/plugins/N-Central/v1/docs/README.md new file mode 100644 index 00000000..db5f00a8 --- /dev/null +++ b/plugins/N-Central/v1/docs/README.md @@ -0,0 +1,59 @@ +Monitor your [N-able N-central](https://www.n-able.com/products/n-central) managed estate in SquaredUp — service organizations, customers, sites, and devices, along with device inventory, service monitoring status, lifecycle info, and active issues — via the [N-central REST API](https://developer.n-able.com/n-central/docs). + +> ⚠️ Requires an N-central server on a version that exposes the REST API (N-central 2023.9 or later). The legacy SOAP API is not supported by this plugin. + +## Setup + +You will need your N-central server's base URL and a **User-API Token**. + +1. Sign in to your N-central server as a user with API access. +2. Go to **Administration → User Management → Users**, click the user you want to use for API access, then go to the **API Access** tab. +3. Click **Generate JSON Web Token** and copy the token — it is shown only once. This is your **User-API Token**. +4. Paste your N-central server's base URL (e.g. `https://yourserver.n-able.com`) into the **Server URL** field, and the User-API Token into the **User-API Token** field. + +The plugin exchanges the User-API Token for a short-lived access token itself via `POST /api/auth/authenticate`, but only when the cached access token has expired — the token is cached and refreshed automatically between requests, so no manual token exchange or periodic refresh is needed. + +## Configuration fields + +| Field | What it is | Where to find it | Required | +| ----- | ---------- | ----------------- | -------- | +| **Server URL** | The base address of your N-central server. | Your browser's address bar when signed in to N-central. | Yes | +| **User-API Token** | A long-lived JWT used to automatically obtain access tokens for every API call. | Administration → User Management → Users → [user] → API Access → Generate JSON Web Token — see Setup above. | Yes | + +On save, the plugin calls `GET /api/service-orgs` (a minimal authenticated probe) to confirm the User-API Token works; an invalid, unreachable, or revoked token fails setup with an authentication error. + +## What this plugin monitors + +- **Organizational structure** — service organizations, customers, and sites, mirroring how your N-central estate is grouped. +- **Devices** — managed workstations, servers, network gear, mobile devices, and cloud services, with hardware/software inventory, service monitoring health, and warranty/lease/lifecycle info per device. +- **Active issues** — currently active alerts per customer, with the affected device and service named. + +The out-of-the-box dashboards include an estate-wide **Overview** plus a perspective for each **Service Organization**, **Customer**, **Site**, and **Device**. + +## Data streams + +- **Device Assets** — hardware and software inventory for a device, one row per asset item (network adapter, patch, service, memory module, etc.). +- **Device Service Monitor Status** — current health of every monitored service/task on a device. +- **Device Lifecycle Info** — warranty, lease, purchase, and replacement dates for a device's hardware. +- **Customer Active Issues** — currently active alerts for a customer, naming the affected device and service. +- **Devices** — managed devices, filterable by customer or by site. +- **Service Org Customers** — customers belonging to a service organization. + +## What gets indexed + +| Object type | API source | Represents | +| ----------- | ---------- | ---------- | +| **Service Organization** | `GET /api/service-orgs` | A top-level MSP business unit. | +| **Customer** | `GET /api/customers` | A managed client. | +| **Site** | `GET /api/sites` | An optional location-based sub-group of a customer. | +| **Device** | `GET /api/devices` | A managed workstation, server, network device, mobile device, or cloud service. | + +**Relationships:** each Customer links to its parent Service Organization; each Site links to its parent Customer; each Device links to its owning Customer, and — when it sits under a site rather than directly under the customer — to that Site as well. + +## Known limitations + +- **Service Organizations without data appear sparse** — the Service Organization perspective and its "Customers" tile depend on your N-central instance having Service Organizations configured; a single-SO or SO-less deployment will show little there even though Customers, Sites, and Devices are unaffected. +- **Sites are optional in N-central** — not every customer has sites configured; the Site perspective is limited to basic properties, as no site-specific monitoring data is exposed by the API. +- **Active issues have no documented severity mapping** — the API's `notificationState` field is a numeric code with no published severity scale, so it's shown as a raw number rather than a health indicator. +- **All data is current-state, not historical** — every stream reflects a snapshot at request time; the N-central REST API does not expose historical/time-series metrics, so no timeframe selection is available on any tile. +- **Read-only** — the plugin never creates, modifies, or deletes anything in N-central. diff --git a/plugins/N-Central/v1/icon.svg b/plugins/N-Central/v1/icon.svg new file mode 100644 index 00000000..d7f0a6ee --- /dev/null +++ b/plugins/N-Central/v1/icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/plugins/N-Central/v1/indexDefinitions/default.json b/plugins/N-Central/v1/indexDefinitions/default.json new file mode 100644 index 00000000..7281a940 --- /dev/null +++ b/plugins/N-Central/v1/indexDefinitions/default.json @@ -0,0 +1,56 @@ +{ + "steps": [ + { + "name": "serviceOrgs", + "dataStream": { "name": "serviceOrgs" }, + "timeframe": "none", + "objectMapping": { + "id": "soId", + "name": "soName", + "type": { "value": "Service Organization" } + } + }, + { + "name": "customers", + "dataStream": { "name": "customers" }, + "timeframe": "none", + "objectMapping": { + "id": "customerId", + "name": "customerName", + "type": { "value": "Customer" }, + "properties": ["parentId"] + } + }, + { + "name": "sites", + "dataStream": { "name": "sites" }, + "timeframe": "none", + "objectMapping": { + "id": "siteId", + "name": "siteName", + "type": { "value": "Site" }, + "properties": ["parentId"] + } + }, + { + "name": "devices", + "dataStream": { "name": "devices" }, + "timeframe": "none", + "objectMapping": { + "id": "deviceId", + "name": "longName", + "type": { "value": "Device" }, + "properties": [ + "customerId", + "siteId", + "soId", + "orgUnitId", + "deviceClass", + "supportedOs", + "isProbe", + "licenseMode" + ] + } + } + ] +} diff --git a/plugins/N-Central/v1/metadata.json b/plugins/N-Central/v1/metadata.json new file mode 100644 index 00000000..96971d70 --- /dev/null +++ b/plugins/N-Central/v1/metadata.json @@ -0,0 +1,49 @@ +{ + "name": "n-central", + "displayName": "N-Able N-central", + "version": "1.0.10", + "author": { "name": "@arobavet", "type": "community" }, + "description": "Monitor your N-able N-central managed estate — service organizations, customers, sites, devices, inventory, service status, and active issues.", + "category": "Monitoring", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": ["rmm", "msp", "monitoring", "devices", "n-able", "n-central"], + "objectTypes": [ + "Service Organization", + "Customer", + "Site", + "Device" + ], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/N-Central/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/N-Central/v1", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "{{host}}", + "authMode": "none", + "headers": [ + { "key": "Accept", "value": "application/json" } + ], + "queryArgs": [], + "scriptingVariables": [ + { "key": "apiBaseUrl", "value": "{{host}}" }, + { "key": "userApiToken", "value": "{{userApiToken}}" } + ], + "preRequestScript": "preRequest.js", + "showScripting": true + } + } +} diff --git a/plugins/N-Central/v1/preRequest.js b/plugins/N-Central/v1/preRequest.js new file mode 100644 index 00000000..6895e719 --- /dev/null +++ b/plugins/N-Central/v1/preRequest.js @@ -0,0 +1,92 @@ +// N-central authenticates by exchanging a long-lived User-API Token (JWT) +// for a short-lived access token via POST /api/auth/authenticate. +// +// The access token is cached in `state` and reused until shortly before +// expiration. When expired, the User-API Token is exchanged again. +// +// The request path is also normalised to avoid duplicated slashes when +// the configured API base URL or endpoint contains a trailing slash. + +url.pathname = url.pathname.replace(/\/{2,}/g, "/"); + +// --- token ----------------------------------------------------------------------- + +if ( + typeof state?.token !== "string" || + (state?.expiryTime ?? 0) <= Date.now() +) { + const authUrl = `${String(secrets.apiBaseUrl|| "") + .trim() + .replace(/\/+$/, "")}/api/auth/authenticate`; + + const userApiToken = String(secrets.userApiToken || "").trim(); + + if (!userApiToken) { + api.report.error( + "N-central User-API Token is not configured." + ); + return; + } + + const request = { + method: "post", + headers: { + Accept: "application/json", + Authorization: `Bearer ${userApiToken}`, + }, + }; + + const response = await fetch(authUrl, request); + + if (!response.ok) { + if (response.status === 401 || response.status === 403) { + api.report.error( + `N-central rejected the User-API Token (HTTP ${response.status}). ` + + "Check that the User-API Token is valid and that the N-central account has API access." + ); + } else if (response.status === 404) { + api.report.error( + `N-central authentication endpoint was not found at ${authUrl} (HTTP 404). ` + + "Check the configured N-central API base URL." + ); + } else { + api.report.error( + `Could not obtain an N-central access token: HTTP ${response.status} ${response.statusText}` + ); + } + + return; + } + + const payload = await response.json(); + + const token = payload?.tokens?.access?.token; + const expirySeconds = payload?.tokens?.access?.expirySeconds; + + if (!token) { + api.report.error( + "N-central returned a successful authentication response but no access token was found." + ); + return; + } + + const lifetimeMs = + typeof expirySeconds === "number" + ? expirySeconds * 1000 + : 3600000; + + // Refresh 5 minutes before expiration so a long-running request is less + // likely to fail because the token expires mid-flight. Clamp the margin + // below lifetimeMs so short-lived tokens still get a future expiryTime. + const refreshMarginMs = Math.min(300000, Math.floor(lifetimeMs / 2)); + + state = { + ...state, + token, + expiryTime: Date.now() + lifetimeMs - refreshMarginMs, + }; +} + +// --- request --------------------------------------------------------------------- + +headers["Authorization"] = `Bearer ${state.token}`; \ No newline at end of file diff --git a/plugins/N-Central/v1/ui.json b/plugins/N-Central/v1/ui.json new file mode 100644 index 00000000..9d9a350f --- /dev/null +++ b/plugins/N-Central/v1/ui.json @@ -0,0 +1,24 @@ +[ + { + "type": "url", + "name": "host", + "label": "Server URL", + "placeholder": "https://yourserver.n-able.com", + "help": "Your N-central server's base address, with no trailing slash.", + "validation": { + "required": true, + "pattern": { + "value": "^https://[^\\s]+$", + "message": "Enter a valid https URL" + } + } + }, + { + "type": "password", + "name": "userApiToken", + "label": "API Token", + "placeholder": "eyJhbGciOiJSUzI1NiJ9...", + "help": "Generated from Administration → User Management → Users → [user] → API Access → Generate JSON Web Token.", + "validation": { "required": true } + } +]