diff --git a/plugins/N-Central/v1/configValidation.json b/plugins/N-Central/v1/configValidation.json
new file mode 100644
index 00000000..5732e140
--- /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 Base 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..b43f84c1
--- /dev/null
+++ b/plugins/N-Central/v1/custom_types.json
@@ -0,0 +1,30 @@
+[
+ {
+ "name": "N-central Service Organization",
+ "sourceType": "N-central Service Organization",
+ "icon": "building",
+ "singular": "Service Organization",
+ "plural": "Service Organizations"
+ },
+ {
+ "name": "N-central Customer",
+ "sourceType": "N-central Customer",
+ "icon": "briefcase",
+ "singular": "Customer",
+ "plural": "Customers"
+ },
+ {
+ "name": "N-central Site",
+ "sourceType": "N-central Site",
+ "icon": "location-dot",
+ "singular": "Site",
+ "plural": "Sites"
+ },
+ {
+ "name": "N-central Device",
+ "sourceType": "N-central 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..804e74cc
--- /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": "200" },
+ "offset": {
+ "mode": "page",
+ "rowCountIn": { "realm": "payloadArraySize", "path": "data" },
+ "base": 1
+ },
+ "out": { "realm": "queryArg", "path": "pageNumber" }
+ }
+ },
+ "matches": { "sourceType": { "type": "oneOf", "values": ["N-central 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": "N-central 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..b61aa819
--- /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": ["N-central 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/deviceLifecycleInfo.json b/plugins/N-Central/v1/dataStreams/deviceLifecycleInfo.json
new file mode 100644
index 00000000..3d8077cb
--- /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": ["N-central 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..ee1dbe78
--- /dev/null
+++ b/plugins/N-Central/v1/dataStreams/deviceList.json
@@ -0,0 +1,62 @@
+{
+ "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 site !== 'undefined' && site && site.length) ? 'api/org-units/' + (Array.isArray(site[0].rawId) ? site[0].rawId[0] : site[0].rawId) + '/devices' : ((typeof customer !== 'undefined' && customer && customer.length) ? 'api/org-units/' + (Array.isArray(customer[0].rawId) ? customer[0].rawId[0] : customer[0].rawId) + '/devices' : 'api/devices') }}",
+ "pathToData": "data",
+ "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": "none",
+ "ui": [
+ {
+ "type": "objects",
+ "name": "customer",
+ "label": "Customer (optional)",
+ "objectLimit": 1,
+ "matches": {
+ "sourceType": { "type": "oneOf", "values": ["N-central Customer"] }
+ }
+ },
+ {
+ "type": "objects",
+ "name": "site",
+ "label": "Site (optional)",
+ "objectLimit": 1,
+ "matches": {
+ "sourceType": { "type": "oneOf", "values": ["N-central Site"] }
+ }
+ }
+ ],
+ "metadata": [
+ {
+ "name": "longName",
+ "displayName": "Device",
+ "shape": "string",
+ "role": "label",
+ "sourceId": "deviceId",
+ "sourceType": "N-central 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..2f64dd50
--- /dev/null
+++ b/plugins/N-Central/v1/dataStreams/deviceServiceMonitorStatus.json
@@ -0,0 +1,47 @@
+{
+ "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": ["N-central 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" },
+ { "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": "string", "role": "id", "visible": false },
+ { "name": "serviceId", "displayName": "Service ID", "shape": "string", "visible": false },
+ { "name": "serviceItemId", "displayName": "Service Item ID", "shape": "string", "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..e6032f20
--- /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": "100" },
+ "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..9abd4cfc
--- /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}-${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..8e4073a4
--- /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": ["N-central Service Organization"] } },
+ "metadata": [
+ {
+ "name": "customerName",
+ "displayName": "Customer",
+ "shape": "string",
+ "role": "label",
+ "sourceId": "customerId",
+ "sourceType": "N-central 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..19c856ae
--- /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": {
+ "customer": {
+ "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..0c65f031
--- /dev/null
+++ b/plugins/N-Central/v1/defaultContent/overview.dash.json
@@ -0,0 +1,364 @@
+{
+ "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": "datastream-properties",
+ "name": "properties"
+ },
+ "scope": {
+ "scope": "{{scopes.[Devices]}}",
+ "workspace": "{{workspaceId}}"
+ },
+ "visualisation": {
+ "type": "data-stream-scalar",
+ "config": {
+ "data-stream-scalar": {
+ "value": { "type": "count" },
+ "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": "19b961fc-0ad8-4d29-9653-1bf0a34f1e77",
+ "x": 1,
+ "y": 2,
+ "w": 1,
+ "h": 2,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Services in Error State",
+ "description": "",
+ "activePluginConfigIds": ["{{configId}}"],
+ "timeframe": "none",
+ "dataStream": {
+ "id": "{{dataStreams.deviceServiceMonitorStatus}}",
+ "name": "deviceServiceMonitorStatus",
+ "pluginConfigId": "{{configId}}",
+ "filter": {
+ "multiOperation": "and",
+ "filters": [
+ { "column": "stateStatus", "operation": "equals", "value": "Failed" }
+ ]
+ }
+ },
+ "scope": {
+ "scope": "{{scopes.[Devices]}}",
+ "workspace": "{{workspaceId}}"
+ },
+ "visualisation": {
+ "type": "data-stream-scalar",
+ "config": {
+ "data-stream-scalar": {
+ "value": { "type": "count" },
+ "comparisonColumn": "none"
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "5ffceefb-2ba4-40a5-99ba-aba28afdbf36",
+ "x": 0,
+ "y": 4,
+ "w": 2,
+ "h": 4,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Service Monitor Status",
+ "description": "",
+ "activePluginConfigIds": ["{{configId}}"],
+ "timeframe": "none",
+ "dataStream": {
+ "id": "{{dataStreams.deviceServiceMonitorStatus}}",
+ "name": "deviceServiceMonitorStatus",
+ "pluginConfigId": "{{configId}}",
+ "group": {
+ "by": [["stateStatus", "uniqueValues"]],
+ "aggregate": [{ "type": "count" }]
+ }
+ },
+ "scope": {
+ "scope": "{{scopes.[Devices]}}",
+ "workspace": "{{workspaceId}}"
+ },
+ "visualisation": {
+ "type": "data-stream-donut-chart",
+ "config": {
+ "data-stream-donut-chart": {
+ "valueColumn": "count",
+ "labelColumn": "stateStatus_uniqueValues",
+ "hideCenterValue": false,
+ "showValuesAsPercentage": false,
+ "legendPosition": "right",
+ "legendMode": "table"
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "d54f1094-36e2-4cb6-b842-385c190a062b",
+ "x": 2,
+ "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..069ffcc8
--- /dev/null
+++ b/plugins/N-Central/v1/defaultContent/scopes.json
@@ -0,0 +1,50 @@
+[
+ {
+ "name": "Service Organizations",
+ "matches": {
+ "sourceType": { "type": "oneOf", "values": ["N-central Service Organization"] }
+ },
+ "variable": {
+ "name": "Service Organization",
+ "allowMultipleSelection": false,
+ "default": "none",
+ "type": "object"
+ }
+ },
+ {
+ "name": "Customers",
+ "matches": {
+ "sourceType": { "type": "oneOf", "values": ["N-central Customer"] }
+ },
+ "variable": {
+ "name": "Customer",
+ "allowMultipleSelection": false,
+ "default": "none",
+ "type": "object"
+ }
+ },
+ {
+ "name": "Sites",
+ "matches": {
+ "sourceType": { "type": "oneOf", "values": ["N-central Site"] }
+ },
+ "variable": {
+ "name": "Site",
+ "allowMultipleSelection": false,
+ "default": "none",
+ "type": "object"
+ }
+ },
+ {
+ "name": "Devices",
+ "matches": {
+ "sourceType": { "type": "oneOf", "values": ["N-central 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..5f4e9fd5
--- /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": {
+ "site": {
+ "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..825bc08b
--- /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 **Base 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 |
+| ----- | ---------- | ----------------- | -------- |
+| **Base 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..ffcad196
--- /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": "N-central Service Organization" }
+ }
+ },
+ {
+ "name": "customers",
+ "dataStream": { "name": "customers" },
+ "timeframe": "none",
+ "objectMapping": {
+ "id": "customerId",
+ "name": "customerName",
+ "type": { "value": "N-central Customer" },
+ "properties": ["parentId"]
+ }
+ },
+ {
+ "name": "sites",
+ "dataStream": { "name": "sites" },
+ "timeframe": "none",
+ "objectMapping": {
+ "id": "siteId",
+ "name": "siteName",
+ "type": { "value": "N-central Site" },
+ "properties": ["parentId"]
+ }
+ },
+ {
+ "name": "devices",
+ "dataStream": { "name": "devices" },
+ "timeframe": "none",
+ "objectMapping": {
+ "id": "deviceId",
+ "name": "longName",
+ "type": { "value": "N-central 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..b5acfd83
--- /dev/null
+++ b/plugins/N-Central/v1/metadata.json
@@ -0,0 +1,49 @@
+{
+ "name": "n-central",
+ "displayName": "N-central",
+ "version": "2.0.1",
+ "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": [
+ "N-central Service Organization",
+ "N-central Customer",
+ "N-central Site",
+ "N-central 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..1847ba22
--- /dev/null
+++ b/plugins/N-Central/v1/ui.json
@@ -0,0 +1,23 @@
+[
+ {
+ "type": "url",
+ "name": "host",
+ "label": "Base 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": "User-API Token",
+ "help": "Generated from Administration → User Management → Users → [user] → API Access → Generate JSON Web Token.",
+ "validation": { "required": true }
+ }
+]
diff --git a/plugins/WasabiWACM/v1/configValidation.json b/plugins/WasabiWACM/v1/configValidation.json
new file mode 100644
index 00000000..0862c5a8
--- /dev/null
+++ b/plugins/WasabiWACM/v1/configValidation.json
@@ -0,0 +1,11 @@
+{
+ "steps": [
+ {
+ "displayName": "Authenticate",
+ "dataStream": { "name": "wacmConfigValidation" },
+ "required": true,
+ "error": "Could not authenticate. Check your username and API key are correct and have not been regenerated.",
+ "success": "Connected successfully."
+ }
+ ]
+}
diff --git a/plugins/WasabiWACM/v1/custom_types.json b/plugins/WasabiWACM/v1/custom_types.json
new file mode 100644
index 00000000..8dd8163c
--- /dev/null
+++ b/plugins/WasabiWACM/v1/custom_types.json
@@ -0,0 +1,37 @@
+[
+ {
+ "name": "Wasabi Standalone Account",
+ "sourceType": "Wasabi Standalone Account",
+ "icon": "id-card",
+ "singular": "Standalone Account",
+ "plural": "Standalone Accounts"
+ },
+ {
+ "name": "Wasabi Control Account",
+ "sourceType": "Wasabi Control Account",
+ "icon": "building-columns",
+ "singular": "Control Account",
+ "plural": "Control Accounts"
+ },
+ {
+ "name": "Wasabi Channel Account",
+ "sourceType": "Wasabi Channel Account",
+ "icon": "handshake",
+ "singular": "Channel Account",
+ "plural": "Channel Accounts"
+ },
+ {
+ "name": "Wasabi Sub-Account",
+ "sourceType": "Wasabi Sub-Account",
+ "icon": "building",
+ "singular": "Sub-Account",
+ "plural": "Sub-Accounts"
+ },
+ {
+ "name": "Wasabi Member",
+ "sourceType": "Wasabi Member",
+ "icon": "user",
+ "singular": "Member",
+ "plural": "Members"
+ }
+]
diff --git a/plugins/WasabiWACM/v1/dataStreams/accounts.json b/plugins/WasabiWACM/v1/dataStreams/accounts.json
new file mode 100644
index 00000000..6a43bed6
--- /dev/null
+++ b/plugins/WasabiWACM/v1/dataStreams/accounts.json
@@ -0,0 +1,36 @@
+{
+ "name": "accounts",
+ "displayName": "Standalone Accounts",
+ "description": "All Wasabi Standalone Accounts, one row per account",
+ "tags": ["Accounts"],
+ "baseDataSourceName": "httpRequestUnscoped",
+ "config": {
+ "httpMethod": "get",
+ "endpointPath": "/api/v1/accounts",
+ "pathToData": "data.items",
+ "paging": {
+ "mode": "offset",
+ "pageSize": { "realm": "queryArg", "path": "size", "value": "100" },
+ "offset": {
+ "mode": "page",
+ "rowCountIn": { "realm": "payloadArraySize", "path": "data.items" },
+ "base": 0
+ },
+ "out": { "realm": "queryArg", "path": "page" }
+ }
+ },
+ "matches": "none",
+ "visibility": { "type": "hidden" },
+ "metadata": [
+ { "name": "id", "displayName": "ID", "shape": "string", "role": "id" },
+ {
+ "name": "name",
+ "displayName": "Name",
+ "computed": true,
+ "valueExpression": "{{ $['companyName'] || (($['firstName'] || '') + ' ' + ($['lastName'] || '')).trim() }}",
+ "role": "label"
+ },
+ { "pattern": ".*" }
+ ],
+ "timeframes": false
+}
diff --git a/plugins/WasabiWACM/v1/dataStreams/channelAccounts.json b/plugins/WasabiWACM/v1/dataStreams/channelAccounts.json
new file mode 100644
index 00000000..9e8bd96e
--- /dev/null
+++ b/plugins/WasabiWACM/v1/dataStreams/channelAccounts.json
@@ -0,0 +1,30 @@
+{
+ "name": "channelAccounts",
+ "displayName": "Channel Accounts",
+ "description": "All Wasabi Channel Accounts, one row per account",
+ "tags": ["Accounts"],
+ "baseDataSourceName": "httpRequestUnscoped",
+ "config": {
+ "httpMethod": "get",
+ "endpointPath": "/api/v1/channel-accounts",
+ "pathToData": "data.items",
+ "paging": {
+ "mode": "offset",
+ "pageSize": { "realm": "queryArg", "path": "size", "value": "100" },
+ "offset": {
+ "mode": "page",
+ "rowCountIn": { "realm": "payloadArraySize", "path": "data.items" },
+ "base": 0
+ },
+ "out": { "realm": "queryArg", "path": "page" }
+ }
+ },
+ "matches": "none",
+ "visibility": { "type": "hidden" },
+ "metadata": [
+ { "name": "id", "displayName": "ID", "shape": "string", "role": "id" },
+ { "name": "name", "displayName": "Name", "shape": "string", "role": "label" },
+ { "pattern": ".*" }
+ ],
+ "timeframes": false
+}
diff --git a/plugins/WasabiWACM/v1/dataStreams/controlAccountBucketUtilization.json b/plugins/WasabiWACM/v1/dataStreams/controlAccountBucketUtilization.json
new file mode 100644
index 00000000..ce6805fc
--- /dev/null
+++ b/plugins/WasabiWACM/v1/dataStreams/controlAccountBucketUtilization.json
@@ -0,0 +1,52 @@
+{
+ "name": "controlAccountBucketUtilization",
+ "displayName": "Control Account Bucket Utilization",
+ "description": "Per-bucket storage utilization for a control account",
+ "tags": ["Accounts", "Usage"],
+ "baseDataSourceName": "httpRequestScopedSingle",
+ "config": {
+ "httpMethod": "get",
+ "endpointPath": "/api/v1/control-accounts/{{object.rawId}}/buckets",
+ "getArgs": [
+ { "key": "from", "value": "{{timeframe.start.split('T')[0]}}" },
+ { "key": "to", "value": "{{timeframe.end.split('T')[0]}}" }
+ ],
+ "pathToData": "data.items",
+ "paging": {
+ "mode": "offset",
+ "pageSize": { "realm": "queryArg", "path": "size", "value": "100" },
+ "offset": {
+ "mode": "page",
+ "rowCountIn": { "realm": "payloadArraySize", "path": "data.items" },
+ "base": 0
+ },
+ "out": { "realm": "queryArg", "path": "page" }
+ }
+ },
+ "matches": { "sourceType": { "type": "oneOf", "values": ["Wasabi Control Account"] } },
+ "metadata": [
+ { "name": "name", "displayName": "Bucket Name", "shape": "string", "role": "label" },
+ { "name": "region", "displayName": "Region", "shape": "string" },
+ { "name": "bucketNumber", "displayName": "Bucket Number", "shape": "string" },
+ { "name": "startTime", "displayName": "Start Time", "shape": "date", "role": "timestamp" },
+ { "name": "endTime", "displayName": "End Time", "shape": "date" },
+ { "name": "activeStorage", "displayName": "Active Storage (TB)", "shape": "number" },
+ { "name": "deletedStorage", "displayName": "Deleted Storage (TB)", "shape": "number" },
+ { "name": "activeObjects", "displayName": "Active Objects", "shape": "number" },
+ { "name": "deletedObjects", "displayName": "Deleted Objects", "shape": "number" },
+ { "name": "apiCalls", "displayName": "API Calls", "shape": "number" },
+ { "name": "egress", "displayName": "Egress (GB)", "shape": "number" },
+ { "name": "ingress", "displayName": "Ingress (GB)", "shape": "number" },
+ { "pattern": ".*" }
+ ],
+ "timeframes": [
+ "last7days",
+ "last30days",
+ "thisMonth",
+ "lastMonth",
+ "thisQuarter",
+ "lastQuarter",
+ "thisYear",
+ "lastYear"
+ ]
+}
diff --git a/plugins/WasabiWACM/v1/dataStreams/controlAccountSummary.json b/plugins/WasabiWACM/v1/dataStreams/controlAccountSummary.json
new file mode 100644
index 00000000..395f5c02
--- /dev/null
+++ b/plugins/WasabiWACM/v1/dataStreams/controlAccountSummary.json
@@ -0,0 +1,42 @@
+{
+ "name": "controlAccountSummary",
+ "displayName": "Control Account Summary",
+ "description": "Current state and storage of a control account",
+ "tags": ["Accounts"],
+ "baseDataSourceName": "httpRequestScopedSingle",
+ "config": {
+ "httpMethod": "get",
+ "endpointPath": "/api/v1/control-accounts/{{object.rawId}}",
+ "getArgs": [{ "key": "includeApiKey", "value": "false" }],
+ "pathToData": "data"
+ },
+ "matches": { "sourceType": { "type": "oneOf", "values": ["Wasabi Control Account"] } },
+ "metadata": [
+ { "name": "id", "displayName": "ID", "shape": "string", "role": "id", "visible": false },
+ { "name": "name", "displayName": "Name", "shape": "string", "role": "label" },
+ {
+ "name": "status",
+ "displayName": "Status",
+ "shape": [
+ "state",
+ {
+ "map": {
+ "success": ["Active"],
+ "error": ["Deactivated"]
+ }
+ }
+ ]
+ },
+ { "name": "subAccountStorage", "displayName": "Sub-Account Storage", "shape": "number" },
+ { "name": "controlAccountStorage", "displayName": "Control Account Storage", "shape": "number" },
+ { "name": "totalStorage", "displayName": "Total Storage", "shape": "number" },
+ { "name": "defaultPurchasedStorageTB", "displayName": "Default Purchased Storage (TB)", "shape": "number" },
+ { "name": "subAccountsCount", "displayName": "Sub-Accounts Count", "shape": "number" },
+ { "name": "channelAccountsCount", "displayName": "Channel Accounts Count", "shape": "number" },
+ { "name": "storageQuotaType", "displayName": "Storage Quota Type", "shape": "string" },
+ { "name": "primaryApiKey", "visible": false },
+ { "name": "secondaryApiKey", "visible": false },
+ { "pattern": ".*" }
+ ],
+ "timeframes": false
+}
diff --git a/plugins/WasabiWACM/v1/dataStreams/controlAccountUsageHistory.json b/plugins/WasabiWACM/v1/dataStreams/controlAccountUsageHistory.json
new file mode 100644
index 00000000..03d14480
--- /dev/null
+++ b/plugins/WasabiWACM/v1/dataStreams/controlAccountUsageHistory.json
@@ -0,0 +1,52 @@
+{
+ "name": "controlAccountUsageHistory",
+ "displayName": "Control Account Usage History",
+ "description": "Periodic storage and traffic usage records for a control account",
+ "tags": ["Accounts", "Usage"],
+ "baseDataSourceName": "httpRequestScopedSingle",
+ "config": {
+ "httpMethod": "get",
+ "endpointPath": "/api/v1/control-accounts/usages",
+ "getArgs": [
+ { "key": "controlAccountId", "value": "{{object.rawId}}" },
+ { "key": "from", "value": "{{timeframe.start.split('T')[0]}}" },
+ { "key": "to", "value": "{{timeframe.end.split('T')[0]}}" }
+ ],
+ "pathToData": "data.items",
+ "paging": {
+ "mode": "offset",
+ "pageSize": { "realm": "queryArg", "path": "size", "value": "100" },
+ "offset": {
+ "mode": "page",
+ "rowCountIn": { "realm": "payloadArraySize", "path": "data.items" },
+ "base": 0
+ },
+ "out": { "realm": "queryArg", "path": "page" }
+ }
+ },
+ "matches": { "sourceType": { "type": "oneOf", "values": ["Wasabi Control Account"] } },
+ "metadata": [
+ { "name": "startTime", "displayName": "Start Time", "shape": "date", "role": "timestamp" },
+ { "name": "endTime", "displayName": "End Time", "shape": "date" },
+ { "name": "activeStorage", "displayName": "Active Storage (TB)", "shape": ["number", { "decimalPlaces": 4 }] },
+ { "name": "deletedStorage", "displayName": "Deleted Storage (TB)", "shape": ["number", { "decimalPlaces": 4 }] },
+ { "name": "storageWrote", "displayName": "Storage Written (TB)", "shape": ["number", { "decimalPlaces": 4 }] },
+ { "name": "storageRead", "displayName": "Storage Read (TB)", "shape": ["number", { "decimalPlaces": 4 }] },
+ { "name": "activeObjects", "displayName": "Active Objects", "shape": "number" },
+ { "name": "deletedObjects", "displayName": "Deleted Objects", "shape": "number" },
+ { "name": "apiCalls", "displayName": "API Calls", "shape": "number" },
+ { "name": "egress", "displayName": "Egress (GB)", "shape": "number" },
+ { "name": "ingress", "displayName": "Ingress (GB)", "shape": "number" },
+ { "pattern": ".*" }
+ ],
+ "timeframes": [
+ "last7days",
+ "last30days",
+ "thisMonth",
+ "lastMonth",
+ "thisQuarter",
+ "lastQuarter",
+ "thisYear",
+ "lastYear"
+ ]
+}
diff --git a/plugins/WasabiWACM/v1/dataStreams/controlAccounts.json b/plugins/WasabiWACM/v1/dataStreams/controlAccounts.json
new file mode 100644
index 00000000..c7e6362c
--- /dev/null
+++ b/plugins/WasabiWACM/v1/dataStreams/controlAccounts.json
@@ -0,0 +1,33 @@
+{
+ "name": "controlAccounts",
+ "displayName": "Control Accounts",
+ "description": "All Wasabi Control Accounts, one row per account",
+ "tags": ["Accounts"],
+ "baseDataSourceName": "httpRequestUnscoped",
+ "config": {
+ "httpMethod": "get",
+ "endpointPath": "/api/v1/control-accounts",
+ "getArgs": [{ "key": "includeApiKey", "value": "false" }],
+ "pathToData": "data.items",
+ "paging": {
+ "mode": "offset",
+ "pageSize": { "realm": "queryArg", "path": "size", "value": "100" },
+ "offset": {
+ "mode": "page",
+ "rowCountIn": { "realm": "payloadArraySize", "path": "data.items" },
+ "base": 0
+ },
+ "out": { "realm": "queryArg", "path": "page" }
+ }
+ },
+ "matches": "none",
+ "visibility": { "type": "hidden" },
+ "metadata": [
+ { "name": "id", "displayName": "ID", "shape": "string", "role": "id" },
+ { "name": "name", "displayName": "Name", "shape": "string", "role": "label" },
+ { "name": "primaryApiKey", "visible": false },
+ { "name": "secondaryApiKey", "visible": false },
+ { "pattern": ".*" }
+ ],
+ "timeframes": false
+}
diff --git a/plugins/WasabiWACM/v1/dataStreams/members.json b/plugins/WasabiWACM/v1/dataStreams/members.json
new file mode 100644
index 00000000..46b62bef
--- /dev/null
+++ b/plugins/WasabiWACM/v1/dataStreams/members.json
@@ -0,0 +1,36 @@
+{
+ "name": "members",
+ "displayName": "Members",
+ "description": "All Wasabi Sub-Account users, one row per member",
+ "tags": ["Accounts", "Users"],
+ "baseDataSourceName": "httpRequestUnscoped",
+ "config": {
+ "httpMethod": "get",
+ "endpointPath": "/api/v1/members",
+ "pathToData": "data.items",
+ "paging": {
+ "mode": "offset",
+ "pageSize": { "realm": "queryArg", "path": "size", "value": "100" },
+ "offset": {
+ "mode": "page",
+ "rowCountIn": { "realm": "payloadArraySize", "path": "data.items" },
+ "base": 0
+ },
+ "out": { "realm": "queryArg", "path": "page" }
+ }
+ },
+ "matches": "none",
+ "visibility": { "type": "hidden" },
+ "metadata": [
+ { "name": "id", "displayName": "ID", "shape": "string", "role": "id" },
+ {
+ "name": "name",
+ "displayName": "Name",
+ "computed": true,
+ "valueExpression": "{{ ((($['firstName'] || '') + ' ' + ($['lastName'] || '')).trim()) || $['username'] }}",
+ "role": "label"
+ },
+ { "pattern": ".*" }
+ ],
+ "timeframes": false
+}
diff --git a/plugins/WasabiWACM/v1/dataStreams/scripts/subAccountInvoices.js b/plugins/WasabiWACM/v1/dataStreams/scripts/subAccountInvoices.js
new file mode 100644
index 00000000..7352f0dd
--- /dev/null
+++ b/plugins/WasabiWACM/v1/dataStreams/scripts/subAccountInvoices.js
@@ -0,0 +1,16 @@
+// Wasabi reports totalStorage/activeStorage/deletedStorage in TB; convert to
+// bytes to match the "bytes" shape used for these fields.
+const TB_TO_BYTES = 1e12;
+
+function toBytes(value) {
+ return typeof value === "number" ? value * TB_TO_BYTES : value;
+}
+
+const items = (data && data.data && data.data.items) || [];
+
+result = items.map((item) => ({
+ ...item,
+ totalStorage: toBytes(item.totalStorage),
+ activeStorage: toBytes(item.activeStorage),
+ deletedStorage: toBytes(item.deletedStorage),
+}));
diff --git a/plugins/WasabiWACM/v1/dataStreams/scripts/subAccountUsageHistory.js b/plugins/WasabiWACM/v1/dataStreams/scripts/subAccountUsageHistory.js
new file mode 100644
index 00000000..03289ad1
--- /dev/null
+++ b/plugins/WasabiWACM/v1/dataStreams/scripts/subAccountUsageHistory.js
@@ -0,0 +1,20 @@
+// Wasabi reports storage fields in TB and traffic fields in GB; convert both
+// to bytes to match the "bytes" shape used for these fields.
+const TB_TO_BYTES = 1e12;
+const GB_TO_BYTES = 1e9;
+
+function scale(value, factor) {
+ return typeof value === "number" ? value * factor : value;
+}
+
+const items = (data && data.data && data.data.items) || [];
+
+result = items.map((item) => ({
+ ...item,
+ activeStorage: scale(item.activeStorage, TB_TO_BYTES),
+ deletedStorage: scale(item.deletedStorage, TB_TO_BYTES),
+ storageWrote: scale(item.storageWrote, TB_TO_BYTES),
+ storageRead: scale(item.storageRead, TB_TO_BYTES),
+ egress: scale(item.egress, GB_TO_BYTES),
+ ingress: scale(item.ingress, GB_TO_BYTES),
+}));
diff --git a/plugins/WasabiWACM/v1/dataStreams/subAccountBucketUtilization.json b/plugins/WasabiWACM/v1/dataStreams/subAccountBucketUtilization.json
new file mode 100644
index 00000000..fab5b09b
--- /dev/null
+++ b/plugins/WasabiWACM/v1/dataStreams/subAccountBucketUtilization.json
@@ -0,0 +1,52 @@
+{
+ "name": "subAccountBucketUtilization",
+ "displayName": "Sub-Account Bucket Utilization",
+ "description": "Per-bucket storage utilization for a sub-account",
+ "tags": ["Accounts", "Usage"],
+ "baseDataSourceName": "httpRequestScopedSingle",
+ "config": {
+ "httpMethod": "get",
+ "endpointPath": "/api/v1/sub-accounts/{{object.rawId}}/buckets",
+ "getArgs": [
+ { "key": "from", "value": "{{timeframe.start.split('T')[0]}}" },
+ { "key": "to", "value": "{{timeframe.end.split('T')[0]}}" }
+ ],
+ "pathToData": "data.items",
+ "paging": {
+ "mode": "offset",
+ "pageSize": { "realm": "queryArg", "path": "size", "value": "100" },
+ "offset": {
+ "mode": "page",
+ "rowCountIn": { "realm": "payloadArraySize", "path": "data.items" },
+ "base": 0
+ },
+ "out": { "realm": "queryArg", "path": "page" }
+ }
+ },
+ "matches": { "sourceType": { "type": "oneOf", "values": ["Wasabi Sub-Account"] } },
+ "metadata": [
+ { "name": "name", "displayName": "Bucket Name", "shape": "string", "role": "label" },
+ { "name": "region", "displayName": "Region", "shape": "string" },
+ { "name": "bucketNumber", "displayName": "Bucket Number", "shape": "string" },
+ { "name": "startTime", "displayName": "Start Time", "shape": "date", "role": "timestamp" },
+ { "name": "endTime", "displayName": "End Time", "shape": "date" },
+ { "name": "activeStorage", "displayName": "Active Storage (TB)", "shape": "number" },
+ { "name": "deletedStorage", "displayName": "Deleted Storage (TB)", "shape": "number" },
+ { "name": "activeObjects", "displayName": "Active Objects", "shape": "number" },
+ { "name": "deletedObjects", "displayName": "Deleted Objects", "shape": "number" },
+ { "name": "apiCalls", "displayName": "API Calls", "shape": "number" },
+ { "name": "egress", "displayName": "Egress (GB)", "shape": "number" },
+ { "name": "ingress", "displayName": "Ingress (GB)", "shape": "number" },
+ { "pattern": ".*" }
+ ],
+ "timeframes": [
+ "last7days",
+ "last30days",
+ "thisMonth",
+ "lastMonth",
+ "thisQuarter",
+ "lastQuarter",
+ "thisYear",
+ "lastYear"
+ ]
+}
diff --git a/plugins/WasabiWACM/v1/dataStreams/subAccountInvoices.json b/plugins/WasabiWACM/v1/dataStreams/subAccountInvoices.json
new file mode 100644
index 00000000..ad95ecc2
--- /dev/null
+++ b/plugins/WasabiWACM/v1/dataStreams/subAccountInvoices.json
@@ -0,0 +1,175 @@
+{
+ "name": "subAccountInvoices",
+ "displayName": "Sub-Account Invoices",
+ "description": "Billed cost line items for a sub-account",
+ "tags": [
+ "Accounts",
+ "Cost"
+ ],
+ "baseDataSourceName": "httpRequestScopedSingle",
+ "config": {
+ "httpMethod": "get",
+ "endpointPath": "/api/v1/invoices",
+ "getArgs": [
+ {
+ "key": "subAccountId",
+ "value": "{{object.rawId}}"
+ },
+ {
+ "key": "from",
+ "value": "{{timeframe.start.split('T')[0]}}"
+ },
+ {
+ "key": "to",
+ "value": "{{timeframe.end.split('T')[0]}}"
+ }
+ ],
+ "pathToData": "data.items",
+ "postRequestScript": "subAccountInvoices.js",
+ "paging": {
+ "mode": "offset",
+ "pageSize": {
+ "realm": "queryArg",
+ "path": "size",
+ "value": "100"
+ },
+ "offset": {
+ "mode": "page",
+ "rowCountIn": {
+ "realm": "payloadArraySize",
+ "path": "data.items"
+ },
+ "base": 0
+ },
+ "out": {
+ "realm": "queryArg",
+ "path": "page"
+ }
+ }
+ },
+ "matches": {
+ "sourceType": {
+ "type": "oneOf",
+ "values": [
+ "Wasabi Sub-Account"
+ ]
+ }
+ },
+ "metadata": [
+ {
+ "name": "periodStart",
+ "displayName": "Period Start",
+ "shape": "date",
+ "role": "timestamp"
+ },
+ {
+ "name": "periodEnd",
+ "displayName": "Period End",
+ "shape": "date"
+ },
+ {
+ "name": "totalStorage",
+ "displayName": "Total Storage",
+ "shape": "bytes"
+ },
+ {
+ "name": "activeStorage",
+ "displayName": "Active Storage",
+ "shape": "bytes"
+ },
+ {
+ "name": "deletedStorage",
+ "displayName": "Deleted Storage",
+ "shape": "bytes"
+ },
+ {
+ "name": "activeStorageTotalCost",
+ "displayName": "Active Storage Cost",
+ "shape": [
+ "currency",
+ {
+ "decimalPlaces": 2,
+ "code": "usd",
+ "thousandsSeparator": true
+ }
+ ]
+ },
+ {
+ "name": "deletedStorageTotalCost",
+ "displayName": "Deleted Storage Cost",
+ "shape": [
+ "currency",
+ {
+ "decimalPlaces": 2,
+ "code": "usd",
+ "thousandsSeparator": true
+ }
+ ]
+ },
+ {
+ "name": "apiCallsTotalCost",
+ "displayName": "API Calls Cost",
+ "shape": [
+ "currency",
+ {
+ "decimalPlaces": 2,
+ "code": "usd",
+ "thousandsSeparator": true
+ }
+ ]
+ },
+ {
+ "name": "ingressTotalCost",
+ "displayName": "Ingress Cost",
+ "shape": [
+ "currency",
+ {
+ "decimalPlaces": 2,
+ "code": "usd",
+ "thousandsSeparator": true
+ }
+ ]
+ },
+ {
+ "name": "egressTotalCost",
+ "displayName": "Egress Cost",
+ "shape": [
+ "currency",
+ {
+ "decimalPlaces": 2,
+ "code": "usd",
+ "thousandsSeparator": true
+ }
+ ]
+ },
+ {
+ "name": "minimumActiveStorageTotalCost",
+ "displayName": "Minimum Active Storage Cost",
+ "shape": [
+ "currency",
+ {
+ "decimalPlaces": 2,
+ "code": "usd",
+ "thousandsSeparator": true
+ }
+ ]
+ },
+ {
+ "name": "subInvoiceId",
+ "displayName": "Sub-Invoice ID",
+ "shape": "string"
+ },
+ {
+ "pattern": ".*"
+ }
+ ],
+ "timeframes": [
+ "last30days",
+ "thisMonth",
+ "lastMonth",
+ "thisQuarter",
+ "lastQuarter",
+ "thisYear",
+ "lastYear"
+ ]
+}
\ No newline at end of file
diff --git a/plugins/WasabiWACM/v1/dataStreams/subAccountSummary.json b/plugins/WasabiWACM/v1/dataStreams/subAccountSummary.json
new file mode 100644
index 00000000..5b11ab1f
--- /dev/null
+++ b/plugins/WasabiWACM/v1/dataStreams/subAccountSummary.json
@@ -0,0 +1,40 @@
+{
+ "name": "subAccountSummary",
+ "displayName": "Sub-Account Summary",
+ "description": "Current state and quota of a sub-account",
+ "tags": ["Accounts"],
+ "baseDataSourceName": "httpRequestScopedSingle",
+ "config": {
+ "httpMethod": "get",
+ "endpointPath": "/api/v1/sub-accounts/{{object.rawId}}",
+ "getArgs": [{ "key": "includeKeys", "value": "false" }],
+ "pathToData": "data"
+ },
+ "matches": { "sourceType": { "type": "oneOf", "values": ["Wasabi Sub-Account"] } },
+ "metadata": [
+ { "name": "id", "displayName": "ID", "shape": "string", "role": "id", "visible": false },
+ { "name": "name", "displayName": "Name", "shape": "string", "role": "label" },
+ {
+ "name": "status",
+ "displayName": "Status",
+ "shape": [
+ "state",
+ {
+ "map": {
+ "success": ["PAID_ACCOUNT", "ON_TRIAL"],
+ "warning": ["SUSPENDED"],
+ "error": ["DEACTIVATED"]
+ }
+ }
+ ]
+ },
+ { "name": "activeStorage", "displayName": "Active Storage", "shape": "number" },
+ { "name": "deletedStorage", "displayName": "Deleted Storage", "shape": "number" },
+ { "name": "purchasedStorageTB", "displayName": "Purchased Storage (TB)", "shape": "number" },
+ { "name": "trialQuotaTB", "displayName": "Trial Quota (TB)", "shape": "number" },
+ { "name": "accessKey", "visible": false },
+ { "name": "secretKey", "visible": false },
+ { "pattern": ".*" }
+ ],
+ "timeframes": false
+}
diff --git a/plugins/WasabiWACM/v1/dataStreams/subAccountUsageHistory.json b/plugins/WasabiWACM/v1/dataStreams/subAccountUsageHistory.json
new file mode 100644
index 00000000..2e196202
--- /dev/null
+++ b/plugins/WasabiWACM/v1/dataStreams/subAccountUsageHistory.json
@@ -0,0 +1,53 @@
+{
+ "name": "subAccountUsageHistory",
+ "displayName": "Sub-Account Usage History",
+ "description": "Periodic storage and traffic usage records for a sub-account",
+ "tags": ["Accounts", "Usage"],
+ "baseDataSourceName": "httpRequestScopedSingle",
+ "config": {
+ "httpMethod": "get",
+ "endpointPath": "/api/v1/usages",
+ "getArgs": [
+ { "key": "subAccountId", "value": "{{object.rawId}}" },
+ { "key": "from", "value": "{{timeframe.start.split('T')[0]}}" },
+ { "key": "to", "value": "{{timeframe.end.split('T')[0]}}" }
+ ],
+ "pathToData": "data.items",
+ "postRequestScript": "subAccountUsageHistory.js",
+ "paging": {
+ "mode": "offset",
+ "pageSize": { "realm": "queryArg", "path": "size", "value": "100" },
+ "offset": {
+ "mode": "page",
+ "rowCountIn": { "realm": "payloadArraySize", "path": "data.items" },
+ "base": 0
+ },
+ "out": { "realm": "queryArg", "path": "page" }
+ }
+ },
+ "matches": { "sourceType": { "type": "oneOf", "values": ["Wasabi Sub-Account"] } },
+ "metadata": [
+ { "name": "startTime", "displayName": "Start Time", "shape": "date", "role": "timestamp" },
+ { "name": "endTime", "displayName": "End Time", "shape": "date" },
+ { "name": "activeStorage", "displayName": "Active Storage", "shape": "bytes" },
+ { "name": "deletedStorage", "displayName": "Deleted Storage", "shape": "bytes" },
+ { "name": "storageWrote", "displayName": "Storage Written", "shape": "bytes" },
+ { "name": "storageRead", "displayName": "Storage Read", "shape": "bytes" },
+ { "name": "activeObjects", "displayName": "Active Objects", "shape": "number" },
+ { "name": "deletedObjects", "displayName": "Deleted Objects", "shape": "number" },
+ { "name": "apiCalls", "displayName": "API Calls", "shape": "number" },
+ { "name": "egress", "displayName": "Egress", "shape": "bytes" },
+ { "name": "ingress", "displayName": "Ingress", "shape": "bytes" },
+ { "pattern": ".*" }
+ ],
+ "timeframes": [
+ "last7days",
+ "last30days",
+ "thisMonth",
+ "lastMonth",
+ "thisQuarter",
+ "lastQuarter",
+ "thisYear",
+ "lastYear"
+ ]
+}
diff --git a/plugins/WasabiWACM/v1/dataStreams/subAccounts.json b/plugins/WasabiWACM/v1/dataStreams/subAccounts.json
new file mode 100644
index 00000000..6efad6da
--- /dev/null
+++ b/plugins/WasabiWACM/v1/dataStreams/subAccounts.json
@@ -0,0 +1,33 @@
+{
+ "name": "subAccounts",
+ "displayName": "Sub-Accounts",
+ "description": "All Wasabi Sub-Accounts, one row per account",
+ "tags": ["Accounts"],
+ "baseDataSourceName": "httpRequestUnscoped",
+ "config": {
+ "httpMethod": "get",
+ "endpointPath": "/api/v1/sub-accounts",
+ "getArgs": [{ "key": "includeKeys", "value": "false" }],
+ "pathToData": "data.items",
+ "paging": {
+ "mode": "offset",
+ "pageSize": { "realm": "queryArg", "path": "size", "value": "100" },
+ "offset": {
+ "mode": "page",
+ "rowCountIn": { "realm": "payloadArraySize", "path": "data.items" },
+ "base": 0
+ },
+ "out": { "realm": "queryArg", "path": "page" }
+ }
+ },
+ "matches": "none",
+ "visibility": { "type": "hidden" },
+ "metadata": [
+ { "name": "id", "displayName": "ID", "shape": "string", "role": "id" },
+ { "name": "name", "displayName": "Name", "shape": "string", "role": "label" },
+ { "name": "accessKey", "visible": false },
+ { "name": "secretKey", "visible": false },
+ { "pattern": ".*" }
+ ],
+ "timeframes": false
+}
diff --git a/plugins/WasabiWACM/v1/dataStreams/wacmConfigValidation.json b/plugins/WasabiWACM/v1/dataStreams/wacmConfigValidation.json
new file mode 100644
index 00000000..5b407e6b
--- /dev/null
+++ b/plugins/WasabiWACM/v1/dataStreams/wacmConfigValidation.json
@@ -0,0 +1,15 @@
+{
+ "name": "wacmConfigValidation",
+ "displayName": "WACM Config Validation",
+ "description": "Reference list of supported storage amount tiers, used to confirm credentials are valid",
+ "tags": ["Reference"],
+ "baseDataSourceName": "httpRequestUnscoped",
+ "config": {
+ "httpMethod": "get",
+ "endpointPath": "/api/v1/sub-accounts/"
+ },
+ "matches": "none",
+ "visibility": { "type": "hidden" },
+ "metadata": [{ "pattern": ".*" }],
+ "timeframes": false
+}
diff --git a/plugins/WasabiWACM/v1/defaultContent/channelAccountDashboard.dash.json b/plugins/WasabiWACM/v1/defaultContent/channelAccountDashboard.dash.json
new file mode 100644
index 00000000..9dfabe8d
--- /dev/null
+++ b/plugins/WasabiWACM/v1/defaultContent/channelAccountDashboard.dash.json
@@ -0,0 +1,49 @@
+{
+ "name": "Channel Account",
+ "schemaVersion": "1.5",
+ "timeframe": "none",
+ "variables": ["{{variables.[Channel Account]}}"],
+ "dashboard": {
+ "_type": "layout/grid",
+ "columns": 4,
+ "version": 1,
+ "contents": [
+ {
+ "i": "05c49dbb-fe02-4afa-94fc-72c40ff22864",
+ "x": 0,
+ "y": 0,
+ "w": 4,
+ "h": 4,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Properties",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "datastream-properties",
+ "name": "properties"
+ },
+ "scope": {
+ "scope": "{{scopes.[Channel Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Channel Account]}}"
+ },
+ "variables": ["{{variables.[Channel Account]}}"],
+ "visualisation": {
+ "type": "data-stream-table",
+ "config": {
+ "data-stream-table": {
+ "transpose": true,
+ "hiddenColumns": ["id", "links", "type", "label", "sourceId", "sourceType"]
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/plugins/WasabiWACM/v1/defaultContent/controlAccountDashboard.dash.json b/plugins/WasabiWACM/v1/defaultContent/controlAccountDashboard.dash.json
new file mode 100644
index 00000000..c40f48ee
--- /dev/null
+++ b/plugins/WasabiWACM/v1/defaultContent/controlAccountDashboard.dash.json
@@ -0,0 +1,299 @@
+{
+ "name": "Control Account",
+ "schemaVersion": "1.5",
+ "timeframe": "last30days",
+ "variables": ["{{variables.[Control Account]}}"],
+ "dashboard": {
+ "_type": "layout/grid",
+ "columns": 4,
+ "version": 1,
+ "contents": [
+ {
+ "i": "b9ee9587-3c24-40d5-ad5d-e3f3b66cd077",
+ "x": 0,
+ "y": 0,
+ "w": 1,
+ "h": 2,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Status",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "{{dataStreams.[controlAccountSummary]}}",
+ "name": "controlAccountSummary",
+ "pluginConfigId": "{{configId}}"
+ },
+ "scope": {
+ "scope": "{{scopes.[Control Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Control Account]}}"
+ },
+ "variables": ["{{variables.[Control Account]}}"],
+ "visualisation": {
+ "type": "data-stream-scalar",
+ "config": {
+ "data-stream-scalar": {
+ "value": "status",
+ "comparisonColumn": "none",
+ "label": "Status"
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "765a8f8e-f694-4f78-a740-1a18770da6da",
+ "x": 1,
+ "y": 0,
+ "w": 1,
+ "h": 2,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Total Storage",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "{{dataStreams.[controlAccountSummary]}}",
+ "name": "controlAccountSummary",
+ "pluginConfigId": "{{configId}}"
+ },
+ "scope": {
+ "scope": "{{scopes.[Control Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Control Account]}}"
+ },
+ "variables": ["{{variables.[Control Account]}}"],
+ "visualisation": {
+ "type": "data-stream-scalar",
+ "config": {
+ "data-stream-scalar": {
+ "value": "totalStorage",
+ "comparisonColumn": "none",
+ "label": "Total Storage"
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "092d2f44-0f35-43cf-8376-d9566be6c82b",
+ "x": 2,
+ "y": 0,
+ "w": 1,
+ "h": 2,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Sub-Accounts Count",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "{{dataStreams.[controlAccountSummary]}}",
+ "name": "controlAccountSummary",
+ "pluginConfigId": "{{configId}}"
+ },
+ "scope": {
+ "scope": "{{scopes.[Control Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Control Account]}}"
+ },
+ "variables": ["{{variables.[Control Account]}}"],
+ "visualisation": {
+ "type": "data-stream-scalar",
+ "config": {
+ "data-stream-scalar": {
+ "value": "subAccountsCount",
+ "comparisonColumn": "none",
+ "label": "Sub-Accounts Count"
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "fc5ad0e6-37cf-422e-a082-02b077ee6b22",
+ "x": 3,
+ "y": 0,
+ "w": 1,
+ "h": 2,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Channel Accounts Count",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "{{dataStreams.[controlAccountSummary]}}",
+ "name": "controlAccountSummary",
+ "pluginConfigId": "{{configId}}"
+ },
+ "scope": {
+ "scope": "{{scopes.[Control Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Control Account]}}"
+ },
+ "variables": ["{{variables.[Control Account]}}"],
+ "visualisation": {
+ "type": "data-stream-scalar",
+ "config": {
+ "data-stream-scalar": {
+ "value": "channelAccountsCount",
+ "comparisonColumn": "none",
+ "label": "Channel Accounts Count"
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "a9e851a7-366f-48fc-8f04-4aa924bfc8fb",
+ "x": 0,
+ "y": 2,
+ "w": 1,
+ "h": 3,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Properties",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "datastream-properties",
+ "name": "properties"
+ },
+ "scope": {
+ "scope": "{{scopes.[Control Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Control Account]}}"
+ },
+ "variables": ["{{variables.[Control Account]}}"],
+ "visualisation": {
+ "type": "data-stream-table",
+ "config": {
+ "data-stream-table": {
+ "transpose": true,
+ "hiddenColumns": ["id", "links", "type", "label", "sourceId", "sourceType"]
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "1e9b4d24-142a-4e76-ac14-59b1c6ace958",
+ "x": 1,
+ "y": 2,
+ "w": 3,
+ "h": 3,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Usage History",
+ "description": "Daily storage usage over the dashboard timeframe",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "{{dataStreams.[controlAccountUsageHistory]}}",
+ "name": "controlAccountUsageHistory",
+ "pluginConfigId": "{{configId}}",
+ "group": {
+ "by": [["startTime", "byDay"]],
+ "aggregate": [
+ { "type": "sum", "names": ["activeStorage"] },
+ { "type": "sum", "names": ["deletedStorage"] }
+ ]
+ },
+ "sort": { "by": [["startTime_byDay", "asc"]] }
+ },
+ "scope": {
+ "scope": "{{scopes.[Control Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Control Account]}}"
+ },
+ "variables": ["{{variables.[Control Account]}}"],
+ "visualisation": {
+ "type": "data-stream-line-graph",
+ "config": {
+ "data-stream-line-graph": {
+ "xAxisColumn": "startTime_byDay",
+ "yAxisColumn": ["activeStorage_sum", "deletedStorage_sum"],
+ "seriesColumn": "none",
+ "showLegend": true,
+ "legendPosition": "bottom",
+ "yAxisLabel": "Storage (TB)",
+ "showYAxisLabel": true,
+ "showTrendLine": false
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "8e648227-c704-4c97-b58d-8ef53fe52a01",
+ "x": 0,
+ "y": 5,
+ "w": 4,
+ "h": 3,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Bucket Utilization",
+ "description": "",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "{{dataStreams.[controlAccountBucketUtilization]}}",
+ "name": "controlAccountBucketUtilization",
+ "pluginConfigId": "{{configId}}",
+ "sort": { "by": [["activeStorage", "desc"]] }
+ },
+ "scope": {
+ "scope": "{{scopes.[Control Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Control Account]}}"
+ },
+ "variables": ["{{variables.[Control Account]}}"],
+ "visualisation": {
+ "type": "data-stream-table",
+ "config": {
+ "data-stream-table": {
+ "transpose": false,
+ "columnOrder": [
+ "name",
+ "region",
+ "activeStorage",
+ "deletedStorage",
+ "activeObjects",
+ "deletedObjects",
+ "apiCalls",
+ "egress",
+ "ingress"
+ ],
+ "hiddenColumns": ["bucketNumber", "startTime", "endTime"]
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/plugins/WasabiWACM/v1/defaultContent/manifest.json b/plugins/WasabiWACM/v1/defaultContent/manifest.json
new file mode 100644
index 00000000..6c9986f4
--- /dev/null
+++ b/plugins/WasabiWACM/v1/defaultContent/manifest.json
@@ -0,0 +1,9 @@
+{
+ "items": [
+ { "name": "overview", "type": "dashboard" },
+ { "name": "controlAccountDashboard", "type": "dashboard" },
+ { "name": "channelAccountDashboard", "type": "dashboard" },
+ { "name": "subAccountDashboard", "type": "dashboard" },
+ { "name": "standaloneAccountDashboard", "type": "dashboard" }
+ ]
+}
diff --git a/plugins/WasabiWACM/v1/defaultContent/overview.dash.json b/plugins/WasabiWACM/v1/defaultContent/overview.dash.json
new file mode 100644
index 00000000..666ac56e
--- /dev/null
+++ b/plugins/WasabiWACM/v1/defaultContent/overview.dash.json
@@ -0,0 +1,291 @@
+{
+ "name": "Overview",
+ "schemaVersion": "1.5",
+ "timeframe": "none",
+ "dashboard": {
+ "_type": "layout/grid",
+ "columns": 4,
+ "version": 1,
+ "contents": [
+ {
+ "i": "7ae96803-bf20-4b4e-a55b-8b601f364657",
+ "x": 0,
+ "y": 0,
+ "w": 1,
+ "h": 2,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Total Sub-Accounts",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "datastream-properties",
+ "name": "properties",
+ "group": { "by": [], "aggregate": [{ "type": "count" }] }
+ },
+ "scope": {
+ "scope": "{{scopes.[Sub-Accounts]}}",
+ "workspace": "{{workspaceId}}"
+ },
+ "visualisation": {
+ "type": "data-stream-scalar",
+ "config": {
+ "data-stream-scalar": {
+ "value": "count",
+ "comparisonColumn": "none",
+ "label": "Total Sub-Accounts"
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "e7bbb607-ca23-4d04-8dd7-122ba6841c51",
+ "x": 1,
+ "y": 0,
+ "w": 1,
+ "h": 2,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Total Active Storage",
+ "description": "Sum of active storage across all sub-accounts",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "datastream-properties",
+ "name": "properties",
+ "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["activeStorage"] }] }
+ },
+ "scope": {
+ "scope": "{{scopes.[Sub-Accounts]}}",
+ "workspace": "{{workspaceId}}"
+ },
+ "visualisation": {
+ "type": "data-stream-scalar",
+ "config": {
+ "data-stream-scalar": {
+ "value": "activeStorage_sum",
+ "comparisonColumn": "none",
+ "label": "Total Active Storage"
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "0e5bb039-c7bc-4dbd-a10e-4fed0213d2db",
+ "x": 2,
+ "y": 0,
+ "w": 1,
+ "h": 2,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Total Control Accounts",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "datastream-properties",
+ "name": "properties",
+ "group": { "by": [], "aggregate": [{ "type": "count" }] }
+ },
+ "scope": {
+ "scope": "{{scopes.[Control Accounts]}}",
+ "workspace": "{{workspaceId}}"
+ },
+ "visualisation": {
+ "type": "data-stream-scalar",
+ "config": {
+ "data-stream-scalar": {
+ "value": "count",
+ "comparisonColumn": "none",
+ "label": "Total Control Accounts"
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "841b58b8-c4e5-4dce-8f39-2dde1f854e01",
+ "x": 3,
+ "y": 0,
+ "w": 1,
+ "h": 2,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Total Channel Accounts",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "datastream-properties",
+ "name": "properties",
+ "group": { "by": [], "aggregate": [{ "type": "count" }] }
+ },
+ "scope": {
+ "scope": "{{scopes.[Channel Accounts]}}",
+ "workspace": "{{workspaceId}}"
+ },
+ "visualisation": {
+ "type": "data-stream-scalar",
+ "config": {
+ "data-stream-scalar": {
+ "value": "count",
+ "comparisonColumn": "none",
+ "label": "Total Channel Accounts"
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "4b6099cd-5684-459e-800f-ec18b11145ba",
+ "x": 0,
+ "y": 2,
+ "w": 1,
+ "h": 3,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Sub-Accounts by Status",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "datastream-properties",
+ "name": "properties",
+ "group": {
+ "by": [["status", "uniqueValues"]],
+ "aggregate": [{ "type": "count" }]
+ }
+ },
+ "scope": {
+ "scope": "{{scopes.[Sub-Accounts]}}",
+ "workspace": "{{workspaceId}}"
+ },
+ "visualisation": {
+ "type": "data-stream-donut-chart",
+ "config": {
+ "data-stream-donut-chart": {
+ "valueColumn": "count",
+ "labelColumn": "status_uniqueValues",
+ "hideCenterValue": false,
+ "showValuesAsPercentage": false,
+ "legendPosition": "auto",
+ "legendMode": "table"
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "8bb333eb-ef71-47a3-bb4c-4dba3c16976e",
+ "x": 1,
+ "y": 2,
+ "w": 3,
+ "h": 3,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Top 10 Sub-Accounts by Active Storage",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "datastream-properties",
+ "name": "properties",
+ "sort": { "by": [["activeStorage", "desc"]], "top": 10 }
+ },
+ "scope": {
+ "scope": "{{scopes.[Sub-Accounts]}}",
+ "workspace": "{{workspaceId}}"
+ },
+ "visualisation": {
+ "type": "data-stream-bar-chart",
+ "config": {
+ "data-stream-bar-chart": {
+ "xAxisData": "name",
+ "yAxisData": ["activeStorage"],
+ "xAxisGroup": "none",
+ "xAxisLabel": "",
+ "yAxisLabel": "Active Storage",
+ "showXAxisLabel": true,
+ "showYAxisLabel": true,
+ "showLegend": false,
+ "legendPosition": "bottom",
+ "showGrid": true,
+ "horizontalLayout": "vertical",
+ "displayMode": "actual",
+ "showTotals": false,
+ "showValue": false,
+ "grouping": false,
+ "range": { "type": "auto" }
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "d009ed2f-31b2-4965-b9d1-16c43574770e",
+ "x": 0,
+ "y": 5,
+ "w": 4,
+ "h": 3,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "All Sub-Accounts",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "datastream-properties",
+ "name": "properties",
+ "sort": { "by": [["activeStorage", "desc"]] }
+ },
+ "scope": {
+ "scope": "{{scopes.[Sub-Accounts]}}",
+ "workspace": "{{workspaceId}}"
+ },
+ "visualisation": {
+ "type": "data-stream-table",
+ "config": {
+ "data-stream-table": {
+ "transpose": false,
+ "columnOrder": [
+ "name",
+ "status",
+ "activeStorage",
+ "deletedStorage",
+ "purchasedStorageTB",
+ "controlAccountName",
+ "channelAccountName"
+ ],
+ "hiddenColumns": ["id", "links", "type", "label", "sourceId", "sourceType"]
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/plugins/WasabiWACM/v1/defaultContent/scopes.json b/plugins/WasabiWACM/v1/defaultContent/scopes.json
new file mode 100644
index 00000000..18bd4f30
--- /dev/null
+++ b/plugins/WasabiWACM/v1/defaultContent/scopes.json
@@ -0,0 +1,50 @@
+[
+ {
+ "name": "Sub-Accounts",
+ "matches": {
+ "sourceType": { "type": "oneOf", "values": ["Wasabi Sub-Account"] }
+ },
+ "variable": {
+ "name": "Sub-Account",
+ "type": "object",
+ "default": "none",
+ "allowMultipleSelection": false
+ }
+ },
+ {
+ "name": "Control Accounts",
+ "matches": {
+ "sourceType": { "type": "oneOf", "values": ["Wasabi Control Account"] }
+ },
+ "variable": {
+ "name": "Control Account",
+ "type": "object",
+ "default": "none",
+ "allowMultipleSelection": false
+ }
+ },
+ {
+ "name": "Channel Accounts",
+ "matches": {
+ "sourceType": { "type": "oneOf", "values": ["Wasabi Channel Account"] }
+ },
+ "variable": {
+ "name": "Channel Account",
+ "type": "object",
+ "default": "none",
+ "allowMultipleSelection": false
+ }
+ },
+ {
+ "name": "Standalone Accounts",
+ "matches": {
+ "sourceType": { "type": "oneOf", "values": ["Wasabi Standalone Account"] }
+ },
+ "variable": {
+ "name": "Standalone Account",
+ "type": "object",
+ "default": "none",
+ "allowMultipleSelection": false
+ }
+ }
+]
diff --git a/plugins/WasabiWACM/v1/defaultContent/standaloneAccountDashboard.dash.json b/plugins/WasabiWACM/v1/defaultContent/standaloneAccountDashboard.dash.json
new file mode 100644
index 00000000..d6a38867
--- /dev/null
+++ b/plugins/WasabiWACM/v1/defaultContent/standaloneAccountDashboard.dash.json
@@ -0,0 +1,49 @@
+{
+ "name": "Standalone Account",
+ "schemaVersion": "1.5",
+ "timeframe": "none",
+ "variables": ["{{variables.[Standalone Account]}}"],
+ "dashboard": {
+ "_type": "layout/grid",
+ "columns": 4,
+ "version": 1,
+ "contents": [
+ {
+ "i": "69bb9842-b955-45c4-aa6a-1f4bdad2e2ad",
+ "x": 0,
+ "y": 0,
+ "w": 4,
+ "h": 4,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Properties",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "datastream-properties",
+ "name": "properties"
+ },
+ "scope": {
+ "scope": "{{scopes.[Standalone Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Standalone Account]}}"
+ },
+ "variables": ["{{variables.[Standalone Account]}}"],
+ "visualisation": {
+ "type": "data-stream-table",
+ "config": {
+ "data-stream-table": {
+ "transpose": true,
+ "hiddenColumns": ["id", "links", "type", "label", "sourceId", "sourceType"]
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/plugins/WasabiWACM/v1/defaultContent/subAccountDashboard.dash.json b/plugins/WasabiWACM/v1/defaultContent/subAccountDashboard.dash.json
new file mode 100644
index 00000000..d8324603
--- /dev/null
+++ b/plugins/WasabiWACM/v1/defaultContent/subAccountDashboard.dash.json
@@ -0,0 +1,344 @@
+{
+ "name": "Sub-Account",
+ "schemaVersion": "1.5",
+ "timeframe": "last30days",
+ "variables": ["{{variables.[Sub-Account]}}"],
+ "dashboard": {
+ "_type": "layout/grid",
+ "columns": 4,
+ "version": 1,
+ "contents": [
+ {
+ "i": "6077a6b9-92ca-4b3a-b3e4-245ddc593596",
+ "x": 0,
+ "y": 0,
+ "w": 1,
+ "h": 2,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Status",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "{{dataStreams.[subAccountSummary]}}",
+ "name": "subAccountSummary",
+ "pluginConfigId": "{{configId}}"
+ },
+ "scope": {
+ "scope": "{{scopes.[Sub-Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Sub-Account]}}"
+ },
+ "variables": ["{{variables.[Sub-Account]}}"],
+ "visualisation": {
+ "type": "data-stream-scalar",
+ "config": {
+ "data-stream-scalar": {
+ "value": "status",
+ "comparisonColumn": "none",
+ "label": "Status"
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "c296d94b-458c-47dd-b85d-d6ff6bb4022e",
+ "x": 1,
+ "y": 0,
+ "w": 1,
+ "h": 2,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Active Storage",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "{{dataStreams.[subAccountSummary]}}",
+ "name": "subAccountSummary",
+ "pluginConfigId": "{{configId}}"
+ },
+ "scope": {
+ "scope": "{{scopes.[Sub-Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Sub-Account]}}"
+ },
+ "variables": ["{{variables.[Sub-Account]}}"],
+ "visualisation": {
+ "type": "data-stream-scalar",
+ "config": {
+ "data-stream-scalar": {
+ "value": "activeStorage",
+ "comparisonColumn": "none",
+ "label": "Active Storage"
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "6a8461bb-722d-4fb6-828b-81d130ad95f5",
+ "x": 2,
+ "y": 0,
+ "w": 1,
+ "h": 2,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Purchased Storage (TB)",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "{{dataStreams.[subAccountSummary]}}",
+ "name": "subAccountSummary",
+ "pluginConfigId": "{{configId}}"
+ },
+ "scope": {
+ "scope": "{{scopes.[Sub-Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Sub-Account]}}"
+ },
+ "variables": ["{{variables.[Sub-Account]}}"],
+ "visualisation": {
+ "type": "data-stream-scalar",
+ "config": {
+ "data-stream-scalar": {
+ "value": "purchasedStorageTB",
+ "comparisonColumn": "none",
+ "label": "Purchased Storage (TB)"
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "df489526-1623-490a-a5a5-f9d27471a1b3",
+ "x": 3,
+ "y": 0,
+ "w": 1,
+ "h": 2,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "MFA Enabled",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "{{dataStreams.[subAccountSummary]}}",
+ "name": "subAccountSummary",
+ "pluginConfigId": "{{configId}}"
+ },
+ "scope": {
+ "scope": "{{scopes.[Sub-Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Sub-Account]}}"
+ },
+ "variables": ["{{variables.[Sub-Account]}}"],
+ "visualisation": {
+ "type": "data-stream-scalar",
+ "config": {
+ "data-stream-scalar": {
+ "value": "mfaEnabled",
+ "comparisonColumn": "none",
+ "label": "MFA Enabled"
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "b93c6b90-4a2d-4c01-97c6-06cb509bf8c0",
+ "x": 0,
+ "y": 2,
+ "w": 1,
+ "h": 3,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Properties",
+ "description": "",
+ "timeframe": "none",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "datastream-properties",
+ "name": "properties"
+ },
+ "scope": {
+ "scope": "{{scopes.[Sub-Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Sub-Account]}}"
+ },
+ "variables": ["{{variables.[Sub-Account]}}"],
+ "visualisation": {
+ "type": "data-stream-table",
+ "config": {
+ "data-stream-table": {
+ "transpose": true,
+ "hiddenColumns": ["id", "links", "type", "label", "sourceId", "sourceType"]
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "4cd92bb1-d28b-4730-aded-3472dc7bc2a1",
+ "x": 1,
+ "y": 2,
+ "w": 3,
+ "h": 3,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Usage History",
+ "description": "Daily storage usage over the dashboard timeframe",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "{{dataStreams.[subAccountUsageHistory]}}",
+ "name": "subAccountUsageHistory",
+ "pluginConfigId": "{{configId}}",
+ "group": {
+ "by": [["startTime", "byDay"]],
+ "aggregate": [
+ { "type": "sum", "names": ["activeStorage"] },
+ { "type": "sum", "names": ["deletedStorage"] }
+ ]
+ },
+ "sort": { "by": [["startTime_byDay", "asc"]] }
+ },
+ "scope": {
+ "scope": "{{scopes.[Sub-Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Sub-Account]}}"
+ },
+ "variables": ["{{variables.[Sub-Account]}}"],
+ "visualisation": {
+ "type": "data-stream-line-graph",
+ "config": {
+ "data-stream-line-graph": {
+ "xAxisColumn": "startTime_byDay",
+ "yAxisColumn": ["activeStorage_sum", "deletedStorage_sum"],
+ "seriesColumn": "none",
+ "showLegend": true,
+ "legendPosition": "bottom",
+ "yAxisLabel": "Storage (TB)",
+ "showYAxisLabel": true,
+ "showTrendLine": false
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "dc8eeb8c-0538-409e-b7ab-5a2f96ff5b26",
+ "x": 0,
+ "y": 5,
+ "w": 2,
+ "h": 3,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Bucket Utilization",
+ "description": "",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "{{dataStreams.[subAccountBucketUtilization]}}",
+ "name": "subAccountBucketUtilization",
+ "pluginConfigId": "{{configId}}",
+ "sort": { "by": [["activeStorage", "desc"]] }
+ },
+ "scope": {
+ "scope": "{{scopes.[Sub-Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Sub-Account]}}"
+ },
+ "variables": ["{{variables.[Sub-Account]}}"],
+ "visualisation": {
+ "type": "data-stream-table",
+ "config": {
+ "data-stream-table": {
+ "transpose": false,
+ "columnOrder": [
+ "name",
+ "region",
+ "activeStorage",
+ "deletedStorage",
+ "activeObjects",
+ "apiCalls",
+ "egress",
+ "ingress"
+ ],
+ "hiddenColumns": ["bucketNumber", "startTime", "endTime", "deletedObjects"]
+ }
+ }
+ }
+ }
+ },
+ {
+ "i": "75641d05-970b-4fee-b43b-3b347a74cf82",
+ "x": 2,
+ "y": 5,
+ "w": 2,
+ "h": 3,
+ "moved": false,
+ "static": false,
+ "z": 0,
+ "config": {
+ "_type": "tile/data-stream",
+ "title": "Invoices",
+ "description": "",
+ "activePluginConfigIds": ["{{configId}}"],
+ "dataStream": {
+ "id": "{{dataStreams.[subAccountInvoices]}}",
+ "name": "subAccountInvoices",
+ "pluginConfigId": "{{configId}}",
+ "sort": { "by": [["periodStart", "desc"]] }
+ },
+ "scope": {
+ "scope": "{{scopes.[Sub-Accounts]}}",
+ "workspace": "{{workspaceId}}",
+ "variable": "{{variables.[Sub-Account]}}"
+ },
+ "variables": ["{{variables.[Sub-Account]}}"],
+ "visualisation": {
+ "type": "data-stream-table",
+ "config": {
+ "data-stream-table": {
+ "transpose": false,
+ "columnOrder": [
+ "periodStart",
+ "periodEnd",
+ "activeStorageTotalCost",
+ "deletedStorageTotalCost",
+ "apiCallsTotalCost",
+ "ingressTotalCost",
+ "egressTotalCost"
+ ],
+ "hiddenColumns": ["subInvoiceId", "minimumActiveStorageTotalCost", "totalStorage", "activeStorage", "deletedStorage"]
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/plugins/WasabiWACM/v1/docs/README.md b/plugins/WasabiWACM/v1/docs/README.md
new file mode 100644
index 00000000..08d79862
--- /dev/null
+++ b/plugins/WasabiWACM/v1/docs/README.md
@@ -0,0 +1,61 @@
+Monitor your [Wasabi](https://wasabi.com) reseller and multi-tenant account hierarchy in SquaredUp — Control Accounts, Channel Accounts, Sub-Accounts, storage usage, bucket utilization, and billing — via the [WACM Connect API](https://docs.wasabi.com/apidocs/wacm-connect-api).
+
+> ⚠️ This plugin covers the **WACM (Wasabi Account Control Manager)** account-management API only, used by Wasabi partners/resellers to manage downstream accounts. It does not cover Wasabi's S3-compatible storage API (buckets/objects data plane) — only account-level storage and billing metrics reported by WACM.
+
+## Setup
+
+You will need a WACM **API key**, paired with the corresponding account name or username as its "username".
+
+1. Sign in to your [Wasabi Account Control Manager console](https://console.wacm.wasabisys.com).
+2. To use an **Account API Key** (recommended for Control/Channel/Sub-Account-wide access): go to the **Account** tab and generate an API key as the Account Admin. Note the **account name** shown alongside it.
+3. Alternatively, to use a **User API Key** (scoped to what that user can see): open your own profile and generate a **User API key**. Note your **WACM username**.
+4. Copy the key — it is shown only once — and paste it, along with the matching account name or username, into the fields below.
+
+> ⚠️ Regenerating an API key permanently invalidates the old one — update this plugin's configuration if you rotate keys.
+
+## Configuration fields
+
+| Field | What it is | Where to find it | Required |
+| ----- | ---------- | ----------------- | -------- |
+| **Username** | The WACM account name (for an Account API Key) or WACM username (for a User API Key). | WACM console → **Account** tab, or your user profile. | Yes |
+| **API Key** | Authenticates every request via HTTP Basic Auth. | WACM console → **Account** tab (Account API Key) or your user profile (User API key). | Yes |
+
+On save, the plugin validates the credentials by calling a lightweight reference endpoint; invalid credentials fail setup with an authentication error.
+
+## What this plugin monitors
+
+- **Account hierarchy** — Standalone Accounts, Control Accounts, Channel Accounts, Sub-Accounts, and Members, with each object's key properties (status, storage quota, contact details).
+- **Storage usage** — daily active/deleted storage, object counts, and traffic (egress/ingress/API calls) history per Sub-Account and Control Account.
+- **Bucket utilization** — daily per-bucket storage breakdowns per Sub-Account and Control Account.
+- **Billing** — monthly invoice line items and costs per Sub-Account.
+
+The out-of-the-box dashboards include an estate-wide **Overview** plus a perspective for each of **Control Account**, **Channel Account**, **Sub-Account**, and **Standalone Account**.
+
+## Data streams
+
+- **Sub-Account Summary** — current status and storage quota, one row per sub-account.
+- **Sub-Account Usage History** — daily storage and traffic usage history for a sub-account.
+- **Sub-Account Bucket Utilization** — daily per-bucket storage utilization for a sub-account.
+- **Sub-Account Invoices** — monthly billed cost line items for a sub-account.
+- **Control Account Summary** — current status and storage totals, one row per control account.
+- **Control Account Usage History** — daily storage and traffic usage history for a control account.
+- **Control Account Bucket Utilization** — daily per-bucket storage utilization for a control account.
+
+## What gets indexed
+
+| Object type | API source | Represents |
+| ----------- | ---------- | ---------- |
+| **Wasabi Standalone Account** | `GET /api/v1/accounts` | An independent Wasabi console account outside the reseller hierarchy. |
+| **Wasabi Control Account** | `GET /api/v1/control-accounts` | The top-level billing-owning account in a reseller hierarchy. |
+| **Wasabi Channel Account** | `GET /api/v1/channel-accounts` | An optional reseller/partner access layer under a Control Account. |
+| **Wasabi Sub-Account** | `GET /api/v1/sub-accounts` | An actual Wasabi Console account — the entity that owns buckets and is billed. |
+| **Wasabi Member** | `GET /api/v1/members` | A user with access to a Sub-Account. |
+
+## Known limitations
+
+- **Read-only** — this plugin never creates, modifies, or deletes anything in Wasabi WACM.
+- **Permission tiers vary by API key.** The WACM API scopes each endpoint to the calling account's tier. A **Sub-Account**-tier key (the most common case) can see its own Sub-Accounts, Channel Accounts, and Members, but **Standalone Accounts and Control Accounts will show no indexed objects** — and their dashboards will stay empty — until a Control-Account-tier or Account-Admin key is used.
+- **Usage and bucket utilization data is daily-granularity** — timeframes shorter than 7 days aren't offered.
+- **Invoices are monthly billing-period records** — timeframes shorter than 30 days aren't offered, and a newly created sub-account may show no invoices until its first billing cycle completes.
+- **Members aren't filtered to a specific Sub-Account** in dashboards — indexed Members can be browsed as their own object type, but no per-sub-account member table is shown on the Sub-Account perspective.
+- **Channel Account and Standalone Account perspectives show only indexed properties** — the WACM API doesn't expose dedicated usage or billing endpoints for these tiers.
diff --git a/plugins/WasabiWACM/v1/icon.svg b/plugins/WasabiWACM/v1/icon.svg
new file mode 100644
index 00000000..a1ed4da7
--- /dev/null
+++ b/plugins/WasabiWACM/v1/icon.svg
@@ -0,0 +1,7 @@
+
diff --git a/plugins/WasabiWACM/v1/indexDefinitions/default.json b/plugins/WasabiWACM/v1/indexDefinitions/default.json
new file mode 100644
index 00000000..af77323b
--- /dev/null
+++ b/plugins/WasabiWACM/v1/indexDefinitions/default.json
@@ -0,0 +1,160 @@
+{
+ "steps": [
+ {
+ "name": "accounts",
+ "dataStream": { "name": "accounts" },
+ "timeframe": "none",
+ "objectMapping": {
+ "id": "id",
+ "name": "name",
+ "type": { "value": "Wasabi Standalone Account" },
+ "properties": [
+ "firstName",
+ "lastName",
+ "companyName",
+ "email",
+ "country",
+ "phoneNumber",
+ "partnerName",
+ "wasabiAccountNumber",
+ "status",
+ "storageAmount"
+ ]
+ },
+ "optional": true
+ },
+ {
+ "name": "controlAccounts",
+ "dataStream": { "name": "controlAccounts" },
+ "timeframe": "none",
+ "objectMapping": {
+ "id": "id",
+ "name": "name",
+ "type": { "value": "Wasabi Control Account" },
+ "properties": [
+ "partnerType",
+ "accountType",
+ "status",
+ "creationDate",
+ "country",
+ "city",
+ "state",
+ "contactEmail",
+ "billingEmail",
+ "governanceAccountId",
+ "governanceAccountName",
+ "channelAccountsCount",
+ "subAccountsCount",
+ "controlAccountEmail",
+ "subAccountStorage",
+ "controlAccountStorage",
+ "totalStorage",
+ "defaultPurchasedStorageTB",
+ "storageQuotaType",
+ "ssoEnabled"
+ ]
+ },
+ "optional": true
+ },
+ {
+ "name": "channelAccounts",
+ "dataStream": { "name": "channelAccounts" },
+ "timeframe": "none",
+ "objectMapping": {
+ "id": "id",
+ "name": "name",
+ "type": { "value": "Wasabi Channel Account" },
+ "properties": [
+ "controlAccountId",
+ "controlAccountName",
+ "governanceAccountId",
+ "governanceAccountName",
+ "purchasedStorage",
+ "subAccountDefaultPurchasedStorage",
+ "subAccountDefaultStorageQuotaType",
+ "storageQuotaType",
+ "subAccountStorage",
+ "billableActiveStorage",
+ "billableDeletedStorage",
+ "subAccountsCount",
+ "status",
+ "creationDate",
+ "country",
+ "city",
+ "state",
+ "contactEmail",
+ "billingEmail",
+ "deleted"
+ ]
+ },
+ "optional": true
+ },
+ {
+ "name": "subAccounts",
+ "dataStream": { "name": "subAccounts" },
+ "timeframe": "none",
+ "objectMapping": {
+ "id": "id",
+ "name": "name",
+ "type": { "value": "Wasabi Sub-Account" },
+ "properties": [
+ "partnerType",
+ "accountType",
+ "status",
+ "creationDate",
+ "country",
+ "city",
+ "state",
+ "contactEmail",
+ "billingEmail",
+ "channelAccountId",
+ "channelAccountName",
+ "channelAccountEmail",
+ "controlAccountId",
+ "controlAccountName",
+ "controlAccountEmail",
+ "governanceAccountId",
+ "governanceAccountName",
+ "wasabiAccountNumber",
+ "wasabiAccountName",
+ "ftpEnabled",
+ "activeStorage",
+ "deletedStorage",
+ "trialQuotaTB",
+ "trialExpiration",
+ "purchasedStorageTB",
+ "mfaEnabled",
+ "storageQuotaType",
+ "ssoEnabled"
+ ]
+ },
+ "optional": true
+ },
+ {
+ "name": "members",
+ "dataStream": { "name": "members" },
+ "timeframe": "none",
+ "objectMapping": {
+ "id": "id",
+ "name": "name",
+ "type": { "value": "Wasabi Member" },
+ "properties": [
+ "subAccountId",
+ "firstName",
+ "lastName",
+ "username",
+ "memberRole",
+ "email",
+ "status",
+ "mfa",
+ "creationDate",
+ "country",
+ "city",
+ "phone",
+ "isSsoUser"
+ ]
+ },
+ "optional": true
+ }
+ ]
+}
diff --git a/plugins/WasabiWACM/v1/metadata.json b/plugins/WasabiWACM/v1/metadata.json
new file mode 100644
index 00000000..5850b149
--- /dev/null
+++ b/plugins/WasabiWACM/v1/metadata.json
@@ -0,0 +1,56 @@
+{
+ "name": "wasabi-wacm",
+ "displayName": "Wasabi WACM",
+ "version": "1.0.1",
+ "author": {
+ "name": "@arobavet",
+ "type": "community"
+ },
+ "description": "Monitor Wasabi Account Control Manager (WACM) accounts, storage usage, bucket utilization and billing across your reseller account hierarchy.",
+ "category": "Cloud Platforms",
+ "type": "hybrid",
+ "schemaVersion": "2.1",
+ "importNotSupported": false,
+ "restrictedToPlatforms": [],
+ "keywords": [
+ "wasabi",
+ "wacm",
+ "cloud storage",
+ "s3",
+ "reseller",
+ "billing",
+ "storage",
+ "account management"
+ ],
+ "objectTypes": [
+ "Wasabi Standalone Account",
+ "Wasabi Control Account",
+ "Wasabi Channel Account",
+ "Wasabi Sub-Account",
+ "Wasabi Member"
+ ],
+ "links": [
+ {
+ "category": "documentation",
+ "url": "https://github.com/squaredup/plugins/blob/main/plugins/WasabiWACM/v1/docs/README.md",
+ "label": "Help adding this plugin"
+ },
+ {
+ "category": "source",
+ "url": "https://github.com/squaredup/plugins/tree/main/plugins/WasabiWACM/v1",
+ "label": "Repository"
+ }
+ ],
+ "base": {
+ "plugin": "WebAPI",
+ "majorVersion": "1",
+ "config": {
+ "baseUrl": "https://api.wacm.wasabisys.com",
+ "authMode": "basic",
+ "basicAuthUsername": "{{username}}",
+ "basicAuthPassword": "{{apiKey}}",
+ "headers": [],
+ "queryArgs": []
+ }
+ }
+}
diff --git a/plugins/WasabiWACM/v1/ui.json b/plugins/WasabiWACM/v1/ui.json
new file mode 100644
index 00000000..59ad915f
--- /dev/null
+++ b/plugins/WasabiWACM/v1/ui.json
@@ -0,0 +1,21 @@
+[
+ {
+ "type": "text",
+ "name": "username",
+ "label": "Username",
+ "help": "Your WACM account name (for an Account API Key) or WACM username (for a User API key).",
+ "placeholder": "e.g. acme-msp or jane.doe",
+ "validation": {
+ "required": true
+ }
+ },
+ {
+ "type": "password",
+ "name": "apiKey",
+ "label": "API Key",
+ "help": "The Account API key or User API key generated in the WACM console — shown only once when created.",
+ "validation": {
+ "required": true
+ }
+ }
+]