From 699e383289a6a02b6cf1383d15a1b48b8ab8a911 Mon Sep 17 00:00:00 2001 From: Tim Wheeler Date: Tue, 1 Sep 2026 17:56:19 +0100 Subject: [PATCH 01/24] Add Redstor plugin (v1) New plugin for Redstor's RedAPI, covering Partner/Customer company hierarchy, backup accounts, backup and restore status, storage consumption, and product subscriptions. Authored directly against Redstor's public RedAPI OpenAPI spec, including a pre-request script for its OAuth2 private_key_jwt auth flow -- unverified against a live tenant, per the README's Known Limitations. Co-Authored-By: Claude Sonnet 5 --- plugins/Redstor/v1/configValidation.json | 18 + plugins/Redstor/v1/custom_types.json | 16 + .../v1/dataStreams/accountBackupErrors.json | 30 ++ .../v1/dataStreams/accountRestoreErrors.json | 30 ++ plugins/Redstor/v1/dataStreams/accounts.json | 40 ++ .../companyAccountBackupStatus.json | 35 ++ .../companyAccountRestoreStatus.json | 35 ++ .../v1/dataStreams/companyBackupStatus.json | 37 ++ .../v1/dataStreams/companyConsumption.json | 47 +++ .../v1/dataStreams/companyCustomers.json | 28 ++ .../v1/dataStreams/companyProfile.json | 22 + .../v1/dataStreams/companyRestoreStatus.json | 36 ++ .../v1/dataStreams/companySubscriptions.json | 43 ++ .../scripts/companyAccountBackupStatus.js | 15 + .../v1/dataStreams/scripts/companyProfile.js | 3 + .../accountPerspective.dash.json | 197 +++++++++ .../companyPerspective.dash.json | 376 ++++++++++++++++++ .../Redstor/v1/defaultContent/manifest.json | 7 + .../v1/defaultContent/overview.dash.json | 376 ++++++++++++++++++ plugins/Redstor/v1/defaultContent/scopes.json | 26 ++ plugins/Redstor/v1/docs/README.md | 66 +++ plugins/Redstor/v1/icon.svg | 3 + .../Redstor/v1/indexDefinitions/default.json | 50 +++ plugins/Redstor/v1/metadata.json | 42 ++ plugins/Redstor/v1/preRequest.js | 150 +++++++ plugins/Redstor/v1/ui.json | 25 ++ 26 files changed, 1753 insertions(+) create mode 100644 plugins/Redstor/v1/configValidation.json create mode 100644 plugins/Redstor/v1/custom_types.json create mode 100644 plugins/Redstor/v1/dataStreams/accountBackupErrors.json create mode 100644 plugins/Redstor/v1/dataStreams/accountRestoreErrors.json create mode 100644 plugins/Redstor/v1/dataStreams/accounts.json create mode 100644 plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json create mode 100644 plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json create mode 100644 plugins/Redstor/v1/dataStreams/companyBackupStatus.json create mode 100644 plugins/Redstor/v1/dataStreams/companyConsumption.json create mode 100644 plugins/Redstor/v1/dataStreams/companyCustomers.json create mode 100644 plugins/Redstor/v1/dataStreams/companyProfile.json create mode 100644 plugins/Redstor/v1/dataStreams/companyRestoreStatus.json create mode 100644 plugins/Redstor/v1/dataStreams/companySubscriptions.json create mode 100644 plugins/Redstor/v1/dataStreams/scripts/companyAccountBackupStatus.js create mode 100644 plugins/Redstor/v1/dataStreams/scripts/companyProfile.js create mode 100644 plugins/Redstor/v1/defaultContent/accountPerspective.dash.json create mode 100644 plugins/Redstor/v1/defaultContent/companyPerspective.dash.json create mode 100644 plugins/Redstor/v1/defaultContent/manifest.json create mode 100644 plugins/Redstor/v1/defaultContent/overview.dash.json create mode 100644 plugins/Redstor/v1/defaultContent/scopes.json create mode 100644 plugins/Redstor/v1/docs/README.md create mode 100644 plugins/Redstor/v1/icon.svg create mode 100644 plugins/Redstor/v1/indexDefinitions/default.json create mode 100644 plugins/Redstor/v1/metadata.json create mode 100644 plugins/Redstor/v1/preRequest.js create mode 100644 plugins/Redstor/v1/ui.json diff --git a/plugins/Redstor/v1/configValidation.json b/plugins/Redstor/v1/configValidation.json new file mode 100644 index 00000000..c3215652 --- /dev/null +++ b/plugins/Redstor/v1/configValidation.json @@ -0,0 +1,18 @@ +{ + "steps": [ + { + "displayName": "Authenticate", + "dataStream": { "name": "companyProfile" }, + "required": true, + "error": "Could not authenticate with Redstor RedAPI. Check the Company ID, Client ID and Private Key.", + "success": "Connected to Redstor successfully." + }, + { + "displayName": "Check customer access", + "dataStream": { "name": "companyCustomers" }, + "required": false, + "error": "Authenticated, but could not list customer companies for this Partner account.", + "success": "Customer companies accessible." + } + ] +} diff --git a/plugins/Redstor/v1/custom_types.json b/plugins/Redstor/v1/custom_types.json new file mode 100644 index 00000000..9c196094 --- /dev/null +++ b/plugins/Redstor/v1/custom_types.json @@ -0,0 +1,16 @@ +[ + { + "name": "Redstor Company", + "sourceType": "Redstor Company", + "icon": "building", + "singular": "Company", + "plural": "Companies" + }, + { + "name": "Redstor Account", + "sourceType": "Redstor Account", + "icon": "cloud-arrow-up", + "singular": "Account", + "plural": "Accounts" + } +] diff --git a/plugins/Redstor/v1/dataStreams/accountBackupErrors.json b/plugins/Redstor/v1/dataStreams/accountBackupErrors.json new file mode 100644 index 00000000..cccc14a1 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/accountBackupErrors.json @@ -0,0 +1,30 @@ +{ + "name": "accountBackupErrors", + "displayName": "Account Backup Errors", + "description": "Backup error and warning messages for an account", + "tags": ["Backup", "Account"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/backups/status/accounts/errors", + "getArgs": [{ "key": "accountId", "value": "{{object.rawId}}" }], + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "pathToData": "results" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Account"] } }, + "metadata": [ + { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, + { "name": "message", "displayName": "Message", "shape": "string", "role": "label" }, + { "name": "category", "displayName": "Category", "shape": "string" }, + { "name": "type", "displayName": "Type", "shape": "string" }, + { "name": "count", "displayName": "Count", "shape": "number" }, + { "name": "extraInfo", "displayName": "Extra Info", "shape": "string", "visible": false }, + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json b/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json new file mode 100644 index 00000000..f2c0af97 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json @@ -0,0 +1,30 @@ +{ + "name": "accountRestoreErrors", + "displayName": "Account Restore Errors", + "description": "Restore error and warning messages for an account", + "tags": ["Backup", "Account"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/restores/status/accounts/errors", + "getArgs": [{ "key": "accountId", "value": "{{object.rawId}}" }], + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "pathToData": "results" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Account"] } }, + "metadata": [ + { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, + { "name": "message", "displayName": "Message", "shape": "string", "role": "label" }, + { "name": "category", "displayName": "Category", "shape": "string" }, + { "name": "type", "displayName": "Type", "shape": "string" }, + { "name": "count", "displayName": "Count", "shape": "number" }, + { "name": "extraInfo", "displayName": "Extra Info", "shape": "string", "visible": false }, + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/accounts.json b/plugins/Redstor/v1/dataStreams/accounts.json new file mode 100644 index 00000000..ae245600 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/accounts.json @@ -0,0 +1,40 @@ +{ + "name": "accounts", + "displayName": "Company Accounts", + "description": "Backup and storage accounts for a Redstor company", + "tags": ["Account"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/storage/accounts", + "getArgs": [{ "key": "companyId", "value": "{{object.rawId}}" }], + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "pathToData": "results" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } }, + "metadata": [ + { "name": "accountId", "displayName": "Account ID", "shape": "guid", "visible": false }, + { "name": "accountName", "displayName": "Account Name", "shape": "string", "role": "label" }, + { "name": "displayName", "displayName": "Display Name", "shape": "string" }, + { "name": "serviceId", "displayName": "Service ID", "shape": "number", "visible": false }, + { "name": "serviceName", "displayName": "Service", "shape": "string" }, + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, + { "name": "productName", "displayName": "Product", "shape": "string" }, + { "name": "storageRegion", "displayName": "Storage Region", "shape": "string" }, + { "name": "createOn", "displayName": "Created On", "shape": "date" }, + { + "name": "companyId", + "displayName": "Company ID", + "computed": true, + "valueExpression": "{{object.rawId}}", + "shape": "guid", + "visible": false + } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json b/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json new file mode 100644 index 00000000..1418c03a --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json @@ -0,0 +1,35 @@ +{ + "name": "companyAccountBackupStatus", + "displayName": "Company Account Backup Status", + "description": "Recent backup operation status history per account for a company", + "tags": ["Backup", "Account"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/backups/status/accounts", + "getArgs": [{ "key": "companyId", "value": "{{object.rawId}}" }], + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "postRequestScript": "companyAccountBackupStatus.js" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } }, + "metadata": [ + { "name": "accountName", "displayName": "Account", "shape": "string", "role": "label" }, + { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, + { + "name": "status", + "displayName": "Status Code", + "shape": "number", + "help": "Raw Redstor status code — RedAPI's public documentation does not define the meaning of each value." + }, + { "name": "message", "displayName": "Message", "shape": "string" }, + { "name": "errorCount", "displayName": "Error Count", "shape": "number" }, + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, + { "name": "accountId", "displayName": "Account ID", "shape": "guid", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json b/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json new file mode 100644 index 00000000..4e82056a --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json @@ -0,0 +1,35 @@ +{ + "name": "companyAccountRestoreStatus", + "displayName": "Company Account Restore Status", + "description": "Current restore operation status per account for a company", + "tags": ["Backup", "Account"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/restores/status/accounts", + "getArgs": [{ "key": "companyId", "value": "{{object.rawId}}" }], + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "pathToData": "results" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } }, + "metadata": [ + { "name": "accountName", "displayName": "Account", "shape": "string", "role": "label" }, + { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, + { + "name": "status", + "displayName": "Status Code", + "shape": "number", + "help": "Raw Redstor status code — RedAPI's public documentation does not define the meaning of each value." + }, + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, + { "name": "accountId", "displayName": "Account ID", "shape": "guid", "visible": false }, + { "name": "companyId", "displayName": "Company ID", "shape": "guid", "visible": false }, + { "name": "groupId", "displayName": "Group ID", "shape": "string", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/companyBackupStatus.json b/plugins/Redstor/v1/dataStreams/companyBackupStatus.json new file mode 100644 index 00000000..581482c0 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companyBackupStatus.json @@ -0,0 +1,37 @@ +{ + "name": "companyBackupStatus", + "displayName": "Company Backup Status", + "description": "Current backup operation status by product for a company", + "tags": ["Backup", "Company"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/backups/status", + "getArgs": [{ "key": "companyId", "value": "{{object.rawId}}" }], + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "pathToData": "results" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } }, + "metadata": [ + { "name": "productName", "displayName": "Product", "shape": "string", "role": "label" }, + { "name": "succeeded", "displayName": "Succeeded", "shape": "number" }, + { "name": "completedWithWarnings", "displayName": "Completed with Warnings", "shape": "number" }, + { "name": "completedWithErrors", "displayName": "Completed with Errors", "shape": "number" }, + { "name": "failed", "displayName": "Failed", "shape": "number" }, + { "name": "missed", "displayName": "Missed", "shape": "number" }, + { + "name": "overallStatus", + "displayName": "Status", + "computed": true, + "valueExpression": "{{ $['failed'] > 0 ? 'error' : (($['completedWithErrors'] > 0 || $['missed'] > 0) ? 'warning' : 'success') }}", + "shape": "state" + }, + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/companyConsumption.json b/plugins/Redstor/v1/dataStreams/companyConsumption.json new file mode 100644 index 00000000..9863a1da --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companyConsumption.json @@ -0,0 +1,47 @@ +{ + "name": "companyConsumption", + "displayName": "Company Consumption", + "description": "Storage consumption and seat usage by product for a company", + "tags": ["Consumption", "Company"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/consumption", + "getArgs": [ + { "key": "companyId", "value": "{{object.rawId}}" }, + { "key": "shouldExcludeTrials", "value": "{{excludeTrials || null}}" } + ], + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "pathToData": "results" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } }, + "ui": [ + { + "type": "checkbox", + "name": "excludeTrials", + "label": "Exclude trial subscriptions", + "value": "true" + } + ], + "metadata": [ + { "name": "productName", "displayName": "Product", "shape": "string", "role": "label" }, + { "name": "sku", "displayName": "SKU", "shape": "string" }, + { "name": "totalDataProtectedInBytes", "displayName": "Data Protected", "shape": "bytes" }, + { "name": "totalDataProtectedInBytesExternal", "displayName": "Data Protected (External)", "shape": "bytes" }, + { "name": "fupOverageInBytes", "displayName": "Fair Use Overage", "shape": "bytes" }, + { "name": "activeSeats", "displayName": "Active Seats", "shape": "number" }, + { "name": "inactiveSeats", "displayName": "Inactive Seats", "shape": "number" }, + { "name": "sharedSeats", "displayName": "Shared Seats", "shape": "number" }, + { "name": "activeWorkloads", "displayName": "Active Workloads", "shape": "number" }, + { "name": "licensedUsers", "displayName": "Licensed Users", "shape": "number" }, + { "name": "isTrial", "displayName": "Trial", "shape": "boolean" }, + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, + { "name": "subscriptionId", "displayName": "Subscription ID", "shape": "guid", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/companyCustomers.json b/plugins/Redstor/v1/dataStreams/companyCustomers.json new file mode 100644 index 00000000..066799bb --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companyCustomers.json @@ -0,0 +1,28 @@ +{ + "name": "companyCustomers", + "displayName": "Company Customers", + "description": "Customer companies under the configured Redstor Partner company", + "tags": ["Company"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/companies/{{dataSource.companyId}}/customers", + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "pathToData": "results" + }, + "matches": "none", + "visibility": { "type": "hidden" }, + "metadata": [ + { "name": "companyId", "displayName": "Company ID", "shape": "guid", "visible": false }, + { "name": "companyName", "displayName": "Company Name", "shape": "string", "role": "label" }, + { "name": "type", "displayName": "Type", "shape": "string" }, + { "name": "isEnabled", "displayName": "Enabled", "shape": "boolean" }, + { "name": "parentCompanyId", "displayName": "Parent Company ID", "shape": "guid", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/companyProfile.json b/plugins/Redstor/v1/dataStreams/companyProfile.json new file mode 100644 index 00000000..ce6aef0d --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companyProfile.json @@ -0,0 +1,22 @@ +{ + "name": "companyProfile", + "displayName": "Company Profile", + "description": "The configured Redstor Partner company's own profile", + "tags": ["Company"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/companies/{{dataSource.companyId}}", + "postRequestScript": "companyProfile.js" + }, + "matches": "none", + "visibility": { "type": "hidden" }, + "metadata": [ + { "name": "companyId", "displayName": "Company ID", "shape": "guid", "visible": false }, + { "name": "companyName", "displayName": "Company Name", "shape": "string", "role": "label" }, + { "name": "type", "displayName": "Type", "shape": "string" }, + { "name": "isEnabled", "displayName": "Enabled", "shape": "boolean" }, + { "name": "parentCompanyId", "displayName": "Parent Company ID", "shape": "guid", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/companyRestoreStatus.json b/plugins/Redstor/v1/dataStreams/companyRestoreStatus.json new file mode 100644 index 00000000..961e5313 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companyRestoreStatus.json @@ -0,0 +1,36 @@ +{ + "name": "companyRestoreStatus", + "displayName": "Company Restore Status", + "description": "Current restore operation status by product for a company", + "tags": ["Backup", "Company"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/restores/status", + "getArgs": [{ "key": "companyId", "value": "{{object.rawId}}" }], + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "pathToData": "results" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } }, + "metadata": [ + { "name": "productName", "displayName": "Product", "shape": "string", "role": "label" }, + { "name": "succeeded", "displayName": "Succeeded", "shape": "number" }, + { "name": "completedWithWarnings", "displayName": "Completed with Warnings", "shape": "number" }, + { "name": "completedWithErrors", "displayName": "Completed with Errors", "shape": "number" }, + { "name": "failed", "displayName": "Failed", "shape": "number" }, + { + "name": "overallStatus", + "displayName": "Status", + "computed": true, + "valueExpression": "{{ $['failed'] > 0 ? 'error' : ($['completedWithErrors'] > 0 ? 'warning' : 'success') }}", + "shape": "state" + }, + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/companySubscriptions.json b/plugins/Redstor/v1/dataStreams/companySubscriptions.json new file mode 100644 index 00000000..3a805d11 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companySubscriptions.json @@ -0,0 +1,43 @@ +{ + "name": "companySubscriptions", + "displayName": "Company Subscriptions", + "description": "Product subscriptions for a company", + "tags": ["Subscriptions", "Company"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/subscriptions", + "getArgs": [{ "key": "companyId", "value": "{{object.rawId}}" }], + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "pathToData": "results" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } }, + "metadata": [ + { "name": "subscriptionId", "displayName": "Subscription ID", "shape": "guid", "role": "label" }, + { "name": "productId", "displayName": "Product ID", "shape": "number" }, + { "name": "editionId", "displayName": "Edition ID", "shape": "number" }, + { "name": "isActive", "displayName": "Active", "shape": "boolean" }, + { + "name": "trialStatus", + "displayName": "Trial Status", + "shape": [ + "state", + { + "map": { + "success": ["Upgraded"], + "unknown": ["NotUsed"], + "warning": ["Trial"], + "error": ["Expired"], + "unmonitored": ["Removed"] + } + } + ] + } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/scripts/companyAccountBackupStatus.js b/plugins/Redstor/v1/dataStreams/scripts/companyAccountBackupStatus.js new file mode 100644 index 00000000..f1843e0d --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/companyAccountBackupStatus.js @@ -0,0 +1,15 @@ +// GET /backups/status/accounts returns one entry per account, each carrying +// a `statusHistory` array of recent runs — flatten to one row per run. +const accounts = data?.results || []; + +result = accounts.flatMap((account) => + (account.statusHistory || []).map((entry) => ({ + accountId: account.accountId, + accountName: account.accountName, + productId: account.productId, + timestamp: entry.timestamp, + status: entry.status, + message: entry.message, + errorCount: entry.errorCount, + })), +); diff --git a/plugins/Redstor/v1/dataStreams/scripts/companyProfile.js b/plugins/Redstor/v1/dataStreams/scripts/companyProfile.js new file mode 100644 index 00000000..7c2b3b44 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/companyProfile.js @@ -0,0 +1,3 @@ +// GET /companies/{companyId} returns a single Company object, not an array; +// wrap it as one row so it can be indexed like every other stream. +result = [data]; diff --git a/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json new file mode 100644 index 00000000..88c4b9fa --- /dev/null +++ b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json @@ -0,0 +1,197 @@ +{ + "name": "Account", + "schemaVersion": "1.5", + "variables": ["{{variables.[Account]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "10564f29-ffb4-4591-b33c-3232a7cf96f8", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Backup Errors", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Account]}}"], + "dataStream": { + "id": "{{dataStreams.accountBackupErrors}}", + "name": "accountBackupErrors", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["count"] }] } + }, + "scope": { + "scope": "{{scopes.[Accounts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Account]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count_sum", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "fe0264d0-3511-48d1-9ce1-a860df56c8a4", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Restore Errors", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Account]}}"], + "dataStream": { + "id": "{{dataStreams.accountRestoreErrors}}", + "name": "accountRestoreErrors", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["count"] }] } + }, + "scope": { + "scope": "{{scopes.[Accounts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Account]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count_sum", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "5900ae96-3e88-4e9f-8d0c-f8a90d1da438", + "x": 0, + "y": 2, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Account Properties", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Account]}}"], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Accounts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Account]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { "transpose": true } + } + } + } + }, + { + "i": "9b8aa0c2-f983-49a2-bffb-866e73dfd37d", + "x": 2, + "y": 2, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Backup Errors", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Account]}}"], + "dataStream": { + "id": "{{dataStreams.accountBackupErrors}}", + "name": "accountBackupErrors", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["timestamp", "desc"]] } + }, + "scope": { + "scope": "{{scopes.[Accounts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Account]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["timestamp", "message", "category", "type", "count"] + } + } + } + } + }, + { + "i": "842007ab-6933-49b0-8a6c-cca0473ef35c", + "x": 0, + "y": 6, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Restore Errors", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Account]}}"], + "dataStream": { + "id": "{{dataStreams.accountRestoreErrors}}", + "name": "accountRestoreErrors", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["timestamp", "desc"]] } + }, + "scope": { + "scope": "{{scopes.[Accounts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Account]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["timestamp", "message", "category", "type", "count"] + } + } + } + } + } + ] + } +} diff --git a/plugins/Redstor/v1/defaultContent/companyPerspective.dash.json b/plugins/Redstor/v1/defaultContent/companyPerspective.dash.json new file mode 100644 index 00000000..591bfc76 --- /dev/null +++ b/plugins/Redstor/v1/defaultContent/companyPerspective.dash.json @@ -0,0 +1,376 @@ +{ + "name": "Company", + "schemaVersion": "1.5", + "variables": ["{{variables.[Company]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "986d50b9-378e-4a2e-ba18-7cce363c75d3", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Accounts", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.accounts}}", + "name": "accounts", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "3918894f-baa9-4f10-be3b-f5e5a53e76c9", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Backup Failures", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companyBackupStatus}}", + "name": "companyBackupStatus", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["failed"] }] } + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "failed_sum", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "8750e03d-1ec0-4391-a7d8-62982b90831a", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Restore Failures", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companyRestoreStatus}}", + "name": "companyRestoreStatus", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["failed"] }] } + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "failed_sum", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "f5cc2719-71c2-465c-993f-5894dc6b5423", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Data Protected", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companyConsumption}}", + "name": "companyConsumption", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["totalDataProtectedInBytes"] }] } + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "totalDataProtectedInBytes_sum", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "cc472807-ff58-4d5c-8b69-ce6dedc36618", + "x": 0, + "y": 2, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Backup Status", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companyBackupStatus}}", + "name": "companyBackupStatus", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "productName", + "overallStatus", + "succeeded", + "completedWithWarnings", + "completedWithErrors", + "failed", + "missed" + ] + } + } + } + } + }, + { + "i": "937fbbe2-5d23-400f-b307-af2c068cfa5f", + "x": 2, + "y": 2, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Restore Status", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companyRestoreStatus}}", + "name": "companyRestoreStatus", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "productName", + "overallStatus", + "succeeded", + "completedWithWarnings", + "completedWithErrors", + "failed" + ] + } + } + } + } + }, + { + "i": "f0405981-4395-44a5-89cd-5fb290f6976e", + "x": 0, + "y": 6, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Consumption", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companyConsumption}}", + "name": "companyConsumption", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "productName", + "totalDataProtectedInBytes", + "activeSeats", + "inactiveSeats", + "sharedSeats", + "activeWorkloads", + "licensedUsers", + "isTrial", + "sku" + ], + "hiddenColumns": ["totalDataProtectedInBytesExternal", "fupOverageInBytes"] + } + } + } + } + }, + { + "i": "7c6937ea-9ef8-44f4-bf6a-596b8f7615bb", + "x": 2, + "y": 6, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Subscriptions", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companySubscriptions}}", + "name": "companySubscriptions", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["productId", "editionId", "trialStatus", "isActive", "subscriptionId"] + } + } + } + } + }, + { + "i": "9de4626d-39c3-408e-b008-248ee2cc737a", + "x": 0, + "y": 10, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Accounts", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.accounts}}", + "name": "accounts", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["accountName", "serviceName", "productName", "storageRegion", "createOn"], + "hiddenColumns": ["displayName"] + } + } + } + } + } + ] + } +} diff --git a/plugins/Redstor/v1/defaultContent/manifest.json b/plugins/Redstor/v1/defaultContent/manifest.json new file mode 100644 index 00000000..f0077666 --- /dev/null +++ b/plugins/Redstor/v1/defaultContent/manifest.json @@ -0,0 +1,7 @@ +{ + "items": [ + { "name": "overview", "type": "dashboard" }, + { "name": "companyPerspective", "type": "dashboard" }, + { "name": "accountPerspective", "type": "dashboard" } + ] +} diff --git a/plugins/Redstor/v1/defaultContent/overview.dash.json b/plugins/Redstor/v1/defaultContent/overview.dash.json new file mode 100644 index 00000000..94ba7b11 --- /dev/null +++ b/plugins/Redstor/v1/defaultContent/overview.dash.json @@ -0,0 +1,376 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "variables": ["{{variables.[Company]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "9795faee-8a6d-46c7-92e8-8d001e6b8983", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Accounts", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.accounts}}", + "name": "accounts", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "8b1f0a2c-aa2a-414a-bbb2-b344f58db72a", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Backup Failures", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companyBackupStatus}}", + "name": "companyBackupStatus", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["failed"] }] } + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "failed_sum", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "b040da9c-f4ae-4818-aa73-620ecb89b204", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Restore Failures", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companyRestoreStatus}}", + "name": "companyRestoreStatus", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["failed"] }] } + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "failed_sum", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "7235ac85-2443-4417-abfa-6e69c9b54ad3", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Data Protected", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companyConsumption}}", + "name": "companyConsumption", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["totalDataProtectedInBytes"] }] } + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "totalDataProtectedInBytes_sum", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "35dd8102-8333-4e6b-8f47-ee6cc6f4365b", + "x": 0, + "y": 2, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Backup Status", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companyBackupStatus}}", + "name": "companyBackupStatus", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "productName", + "overallStatus", + "succeeded", + "completedWithWarnings", + "completedWithErrors", + "failed", + "missed" + ] + } + } + } + } + }, + { + "i": "74364c93-31bd-4e81-8c00-87b79b183990", + "x": 2, + "y": 2, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Restore Status", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companyRestoreStatus}}", + "name": "companyRestoreStatus", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "productName", + "overallStatus", + "succeeded", + "completedWithWarnings", + "completedWithErrors", + "failed" + ] + } + } + } + } + }, + { + "i": "814ace7e-f1e7-47b1-89e4-e1312e1e79ab", + "x": 0, + "y": 6, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Consumption", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companyConsumption}}", + "name": "companyConsumption", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "productName", + "totalDataProtectedInBytes", + "activeSeats", + "inactiveSeats", + "sharedSeats", + "activeWorkloads", + "licensedUsers", + "isTrial", + "sku" + ], + "hiddenColumns": ["totalDataProtectedInBytesExternal", "fupOverageInBytes"] + } + } + } + } + }, + { + "i": "54c3dab5-4717-4d96-9b89-5a301fd7085e", + "x": 2, + "y": 6, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Subscriptions", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companySubscriptions}}", + "name": "companySubscriptions", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["productId", "editionId", "trialStatus", "isActive", "subscriptionId"] + } + } + } + } + }, + { + "i": "3d81b3b7-34d2-4ad0-ad7f-bb30b18bb7ff", + "x": 0, + "y": 10, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Accounts", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.accounts}}", + "name": "accounts", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["accountName", "serviceName", "productName", "storageRegion", "createOn"], + "hiddenColumns": ["displayName"] + } + } + } + } + } + ] + } +} diff --git a/plugins/Redstor/v1/defaultContent/scopes.json b/plugins/Redstor/v1/defaultContent/scopes.json new file mode 100644 index 00000000..ba7615e6 --- /dev/null +++ b/plugins/Redstor/v1/defaultContent/scopes.json @@ -0,0 +1,26 @@ +[ + { + "name": "Companies", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } + }, + "variable": { + "name": "Company", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Accounts", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Redstor Account"] } + }, + "variable": { + "name": "Account", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/Redstor/v1/docs/README.md b/plugins/Redstor/v1/docs/README.md new file mode 100644 index 00000000..51d377ef --- /dev/null +++ b/plugins/Redstor/v1/docs/README.md @@ -0,0 +1,66 @@ +Monitor your [Redstor](https://www.redstor.com) Partner account in SquaredUp — customer companies, backup accounts, backup and restore status, storage consumption, and product subscriptions — via the [RedAPI](https://www.redstor.com/our-technology/integrations/redapi/). + +> ⚠️ RedAPI is only available to **Partner Admin** users on current Redstor pricing plans — Company Admins and legacy-plan accounts cannot use it. This plugin does not cover Redstor's older Storage Platform REST API. + +## Setup + +You will need a RedAPI **service account** with its Client ID and private key, plus your Partner company's ID. + +1. Sign in to [RedApp](https://redapp.redstor.com) as a **Partner Admin**. +2. Note your Partner company's ID — find it under **Company Settings** — and paste it into the **Company ID** field. +3. Go to **RedAPI → Service accounts** and click to add a new service account. Give it a descriptive name and assign it access to your Partner company (and the customers you want visibility into). +4. Under the service account, create a **key**. Redstor generates a Client ID and a private/public key pair — download the private key file. +5. Copy the **Client ID** into the **Client ID** field. +6. Open the downloaded private key file and paste its full contents — including the `-----BEGIN PRIVATE KEY-----` / `-----END PRIVATE KEY-----` lines — into the **Private Key** field. + +## Configuration fields + +| Field | What it is | Where to find it | Required | +| ----- | ---------- | ---------------- | -------- | +| **Company ID** | Your Redstor Partner company's ID. Scopes every API call to this company and its direct customers. | RedApp → **Company Settings**. | Yes | +| **Client ID** | Identifies the RedAPI service account used to authenticate. | RedApp → **RedAPI → Service accounts** → your service account's key. | Yes | +| **Private Key** | The PEM-encoded private key paired with the Client ID; signs the client assertion used to obtain access tokens. | Downloaded when the service account's key was created. | Yes | + +On save, the plugin authenticates by exchanging a signed JWT for a Redstor access token and calling your company's profile; an invalid Company ID, Client ID, or Private Key fails setup with an authentication error. + +## What this plugin monitors + +- **Companies** — your Partner account and its customer companies. +- **Accounts** — backup and storage accounts, one per product/service a company runs. +- **Backup & restore status** — succeeded/failed/missed counts by product, plus recent per-account run history. +- **Consumption & subscriptions** — storage and seat usage, and active product subscriptions, per company. +- **Account errors** — backup and restore error and warning messages for a specific account. + +The out-of-the-box dashboards include a Partner-wide **Overview** plus a perspective for each **Company** and **Account**. + +## Data streams + +- **Company Accounts** — backup/storage accounts for a company, one row per account. +- **Company Backup Status** — current backup status by product for a company. +- **Company Restore Status** — current restore status by product for a company. +- **Company Consumption** — storage and seat consumption by product for a company. +- **Company Subscriptions** — product subscriptions for a company. +- **Company Account Backup Status** — recent backup run history per account for a company. +- **Company Account Restore Status** — current restore status per account for a company. +- **Account Backup Errors** — backup error and warning messages for an account. +- **Account Restore Errors** — restore error and warning messages for an account. + +## What gets indexed + +| Object type | API source | Represents | +| ----------- | ---------- | ---------- | +| **Redstor Company** | `GET /companies/{companyId}`, `GET /companies/{companyId}/customers` | Your configured Partner company and its direct customer companies. | +| **Redstor Account** | `GET /storage/accounts` | A backup or storage account belonging to a company, for one product/service. | + +**Relationships:** each Account belongs to its parent Company. + +## Known limitations + +- **Entirely unverified against a live Redstor tenant** — this plugin was authored directly from Redstor's public RedAPI OpenAPI specification, with no Redstor Partner Admin credentials available to test authentication or any data stream against a real account. Verify against a live tenant before relying on it in production. +- **No historical or time-range data** — RedAPI exposes no `from`/`to` range parameter on any endpoint; every stream returns a current-state snapshot, and dashboards have no timeframe picker. +- **Backup/restore status codes are undocumented** — the numeric `status` values in Company Account Backup/Restore Status aren't defined anywhere in RedAPI's public documentation, so they're shown as raw numbers rather than mapped to a health color. +- **Subscriptions show IDs, not names** — RedAPI's `/subscriptions` endpoint returns `productId`/`editionId` only, with no product name lookup; this plugin doesn't index a Products type, so subscription rows show numeric IDs. +- **Company hierarchy is one level deep** — only the configured Partner company and its direct customers are indexed; deeper reseller-of-reseller chains aren't walked recursively. +- **Private key format assumed PEM** — the exact format RedApp downloads for a RedAPI service account key couldn't be confirmed without live access; PEM is assumed based on standard `private_key_jwt` practice. +- **Subject to Redstor's fair-use throttling** — RedAPI enforces rate limiting; specific limits aren't published. +- **Read-only** — the plugin never creates, modifies, or deletes anything in Redstor. diff --git a/plugins/Redstor/v1/icon.svg b/plugins/Redstor/v1/icon.svg new file mode 100644 index 00000000..afa341e9 --- /dev/null +++ b/plugins/Redstor/v1/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/Redstor/v1/indexDefinitions/default.json b/plugins/Redstor/v1/indexDefinitions/default.json new file mode 100644 index 00000000..59021449 --- /dev/null +++ b/plugins/Redstor/v1/indexDefinitions/default.json @@ -0,0 +1,50 @@ +{ + "steps": [ + { + "name": "companyProfile", + "dataStream": { "name": "companyProfile" }, + "timeframe": "none", + "objectMapping": { + "id": "companyId", + "name": "companyName", + "type": { "value": "Redstor Company" }, + "properties": ["type", "isEnabled", "parentCompanyId"] + } + }, + { + "name": "companyCustomers", + "dataStream": { "name": "companyCustomers" }, + "timeframe": "none", + "objectMapping": { + "id": "companyId", + "name": "companyName", + "type": { "value": "Redstor Company" }, + "properties": ["type", "isEnabled", "parentCompanyId"] + } + }, + { + "name": "accounts", + "dataStream": { "name": "accounts" }, + "scope": { + "query": "g.V().has(\"sourceType\", \"Redstor Company\")" + }, + "timeframe": "none", + "objectMapping": { + "id": "accountId", + "name": "accountName", + "type": { "value": "Redstor Account" }, + "properties": [ + "displayName", + "serviceId", + "serviceName", + "productId", + "productName", + "storageRegion", + "createOn", + "companyId" + ] + }, + "dependsOn": ["companyProfile", "companyCustomers"] + } + ] +} diff --git a/plugins/Redstor/v1/metadata.json b/plugins/Redstor/v1/metadata.json new file mode 100644 index 00000000..a16c1718 --- /dev/null +++ b/plugins/Redstor/v1/metadata.json @@ -0,0 +1,42 @@ +{ + "name": "redstor", + "displayName": "Redstor", + "version": "1.0.0", + "author": { "name": "@TimWheeler-SQUP", "type": "community" }, + "description": "Monitor your Redstor RedAPI Partner account — customer companies, backup accounts, backup and restore status, storage consumption, and product subscriptions.", + "category": "Monitoring", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": ["backup", "disaster recovery", "archiving", "msp", "redstor", "redapi"], + "objectTypes": ["Redstor Company", "Redstor Account"], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/Redstor/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/Redstor/v1", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "https://api.redstor.com", + "authMode": "none", + "headers": [{ "key": "Accept", "value": "application/json" }], + "queryArgs": [], + "scriptingVariables": [ + { "key": "clientId", "value": "{{clientId}}" }, + { "key": "privateKey", "value": "{{privateKey}}" } + ], + "preRequestScript": "preRequest.js", + "showScripting": true + } + } +} diff --git a/plugins/Redstor/v1/preRequest.js b/plugins/Redstor/v1/preRequest.js new file mode 100644 index 00000000..285a380a --- /dev/null +++ b/plugins/Redstor/v1/preRequest.js @@ -0,0 +1,150 @@ +// Redstor RedAPI authenticates using OAuth2 client_credentials with the +// `private_key_jwt` client-authentication method (RFC 7523 / OIDC): a JWT +// assertion is signed with a service account's private key and exchanged +// at the token endpoint for a short-lived access token. +// +// Unlike a standard OAuth2 flow, the resulting access token is NOT sent as +// `Authorization: Bearer` on API calls — RedAPI's OpenAPI spec declares the +// security scheme as an `X-Api-Key` header, so that's where the token goes. +// +// The access token is cached in `state` and reused until shortly before +// expiration; the client-assertion JWT itself is re-signed on every +// exchange (it is short-lived by design, not cached). + +const TOKEN_ENDPOINT = "https://id.redstor.com/connect/token"; + +// --- helpers ----------------------------------------------------------------------- + +function pemToArrayBuffer(pem) { + const base64 = String(pem) + .replace(/-----BEGIN [^-]+-----/, "") + .replace(/-----END [^-]+-----/, "") + .replace(/\s+/g, ""); + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes.buffer; +} + +function bytesToBase64Url(bytes) { + let binary = ""; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} + +function utf8ToBytes(str) { + return new TextEncoder().encode(str); +} + +async function signClientAssertion(clientId, privateKeyPem) { + const header = { alg: "RS256", typ: "JWT" }; + const nowSeconds = Math.floor(Date.now() / 1000); + const payload = { + iss: clientId, + sub: clientId, + aud: TOKEN_ENDPOINT, + jti: crypto.randomUUID(), + iat: nowSeconds, + // Short-lived on purpose: this JWT authenticates the token-exchange + // request itself, not subsequent API calls. + exp: nowSeconds + 300, + }; + + const encodedHeader = bytesToBase64Url(utf8ToBytes(JSON.stringify(header))); + const encodedPayload = bytesToBase64Url(utf8ToBytes(JSON.stringify(payload))); + const signingInput = `${encodedHeader}.${encodedPayload}`; + + const key = await crypto.subtle.importKey( + "pkcs8", + pemToArrayBuffer(privateKeyPem), + { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, + false, + ["sign"], + ); + + const signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + key, + utf8ToBytes(signingInput), + ); + + return `${signingInput}.${bytesToBase64Url(new Uint8Array(signature))}`; +} + +// --- token ----------------------------------------------------------------------- + +if (typeof state?.token !== "string" || (state?.expiryTime ?? 0) <= Date.now()) { + const clientId = String(secrets.clientId || "").trim(); + const privateKey = String(secrets.privateKey || "").trim(); + + if (!clientId || !privateKey) { + api.report.error("Redstor RedAPI Client ID or Private Key is not configured."); + return; + } + + let clientAssertion; + try { + clientAssertion = await signClientAssertion(clientId, privateKey); + } catch (e) { + api.report.error( + `Could not sign the Redstor client assertion — check the Private Key is a valid PEM-encoded RSA private key. ${e}`, + ); + return; + } + + const body = new URLSearchParams({ + grant_type: "client_credentials", + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_assertion: clientAssertion, + scope: "api.read", + }).toString(); + + const response = await fetch(TOKEN_ENDPOINT, { + method: "post", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body, + }); + + if (!response.ok) { + if (response.status === 400 || response.status === 401) { + api.report.error( + `Redstor rejected the RedAPI service account credentials (HTTP ${response.status}). Check the Client ID and Private Key.`, + ); + } else { + api.report.error( + `Could not obtain a Redstor access token: HTTP ${response.status} ${response.statusText}`, + ); + } + return; + } + + const payload = await response.json(); + const token = payload?.access_token; + const expiresIn = payload?.expires_in; + + if (!token) { + api.report.error( + "Redstor returned a successful token response but no access_token was found.", + ); + return; + } + + const lifetimeMs = typeof expiresIn === "number" ? expiresIn * 1000 : 3600000; + // Refresh 5 minutes before expiration so a long-running request is less + // likely to fail because the token expires mid-flight. + const refreshMarginMs = Math.min(300000, Math.floor(lifetimeMs / 2)); + + state = { + ...state, + token, + expiryTime: Date.now() + lifetimeMs - refreshMarginMs, + }; +} + +// --- request --------------------------------------------------------------------- + +headers["X-Api-Key"] = state.token; diff --git a/plugins/Redstor/v1/ui.json b/plugins/Redstor/v1/ui.json new file mode 100644 index 00000000..817eda60 --- /dev/null +++ b/plugins/Redstor/v1/ui.json @@ -0,0 +1,25 @@ +[ + { + "type": "text", + "name": "companyId", + "label": "Company ID", + "placeholder": "e.g. 3fa85f64-5717-4562-b3fc-2c963f66afa6", + "help": "Your Redstor **Partner** company's ID in RedApp — found under **RedApp → Company Settings**. Every request is scoped to this company and its direct customers.", + "validation": { "required": true } + }, + { + "type": "text", + "name": "clientId", + "label": "Client ID", + "help": "The Client ID of the RedAPI service account, created in RedApp → **RedAPI → Service accounts**.", + "validation": { "required": true } + }, + { + "type": "textarea", + "name": "privateKey", + "label": "Private Key", + "rows": 12, + "help": "The PEM-encoded private key downloaded when you created the service account's key in RedApp → **RedAPI → Service accounts**. Keep this secret — it authenticates as your Partner Admin. Paste the full key including the `-----BEGIN PRIVATE KEY-----` / `-----END PRIVATE KEY-----` lines.", + "validation": { "required": true } + } +] From 3c1fba5165e7d1cab717886260a2f71de4d8114e Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:43:48 +0100 Subject: [PATCH 02/24] Fix Redstor review findings: dashboards, naming, security, and API verification - Consolidate duplicate Overview/Company dashboards and switch Backup/Restore Status tiles to stacked percentage bar charts, matching the real RedApp UI - De-prefix source types (Redstor Company/Account -> Company/Account) per REVIEW.md, cascading through custom_types, index defs, streams, and scopes - Encrypt the Private Key field and add a missing Client ID placeholder - Add CODEOWNERS entry, reuse the existing "Usage" tag, add dashboard-level timeframe: none - Verify every endpoint, param, and response field against Redstor's public RedAPI swagger spec; fix a dead checkbox default and map the now-documented Backup Status codes to a status color, and correct the docs accordingly Co-Authored-By: Claude Sonnet 5 --- .github/CODEOWNERS | 1 + plugins/Redstor/v1/custom_types.json | 4 +- .../v1/dataStreams/accountBackupErrors.json | 2 +- .../v1/dataStreams/accountRestoreErrors.json | 2 +- plugins/Redstor/v1/dataStreams/accounts.json | 2 +- .../companyAccountBackupStatus.json | 12 +- .../companyAccountRestoreStatus.json | 2 +- .../v1/dataStreams/companyBackupStatus.json | 2 +- .../v1/dataStreams/companyConsumption.json | 6 +- .../v1/dataStreams/companyRestoreStatus.json | 2 +- .../v1/dataStreams/companySubscriptions.json | 2 +- .../accountPerspective.dash.json | 1 + .../companyPerspective.dash.json | 376 ------------------ .../Redstor/v1/defaultContent/manifest.json | 1 - .../v1/defaultContent/overview.dash.json | 52 +-- plugins/Redstor/v1/defaultContent/scopes.json | 4 +- plugins/Redstor/v1/docs/README.md | 10 +- .../Redstor/v1/indexDefinitions/default.json | 8 +- plugins/Redstor/v1/metadata.json | 2 +- plugins/Redstor/v1/ui.json | 2 + 20 files changed, 67 insertions(+), 426 deletions(-) delete mode 100644 plugins/Redstor/v1/defaultContent/companyPerspective.dash.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c44719cf..ed015e12 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,6 +17,7 @@ plugins/MetOffice/* @blackgrouse plugins/Phare/* @vinbab plugins/Postcoder/* @richbenwell plugins/RDAP/* @richbenwell +plugins/Redstor/* @TimWheeler-SQUP plugins/RSS/* @clarkd plugins/Rootly/* @noorulkhan-n plugins/SendGrid/* @richbenwell diff --git a/plugins/Redstor/v1/custom_types.json b/plugins/Redstor/v1/custom_types.json index 9c196094..b3d0a061 100644 --- a/plugins/Redstor/v1/custom_types.json +++ b/plugins/Redstor/v1/custom_types.json @@ -1,14 +1,14 @@ [ { "name": "Redstor Company", - "sourceType": "Redstor Company", + "sourceType": "Company", "icon": "building", "singular": "Company", "plural": "Companies" }, { "name": "Redstor Account", - "sourceType": "Redstor Account", + "sourceType": "Account", "icon": "cloud-arrow-up", "singular": "Account", "plural": "Accounts" diff --git a/plugins/Redstor/v1/dataStreams/accountBackupErrors.json b/plugins/Redstor/v1/dataStreams/accountBackupErrors.json index cccc14a1..65cc48ce 100644 --- a/plugins/Redstor/v1/dataStreams/accountBackupErrors.json +++ b/plugins/Redstor/v1/dataStreams/accountBackupErrors.json @@ -16,7 +16,7 @@ }, "pathToData": "results" }, - "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Account"] } }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Account"] } }, "metadata": [ { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, { "name": "message", "displayName": "Message", "shape": "string", "role": "label" }, diff --git a/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json b/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json index f2c0af97..43bf958e 100644 --- a/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json +++ b/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json @@ -16,7 +16,7 @@ }, "pathToData": "results" }, - "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Account"] } }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Account"] } }, "metadata": [ { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, { "name": "message", "displayName": "Message", "shape": "string", "role": "label" }, diff --git a/plugins/Redstor/v1/dataStreams/accounts.json b/plugins/Redstor/v1/dataStreams/accounts.json index ae245600..4e34cff0 100644 --- a/plugins/Redstor/v1/dataStreams/accounts.json +++ b/plugins/Redstor/v1/dataStreams/accounts.json @@ -16,7 +16,7 @@ }, "pathToData": "results" }, - "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, "metadata": [ { "name": "accountId", "displayName": "Account ID", "shape": "guid", "visible": false }, { "name": "accountName", "displayName": "Account Name", "shape": "string", "role": "label" }, diff --git a/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json b/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json index 1418c03a..aadf817a 100644 --- a/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json +++ b/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json @@ -16,15 +16,23 @@ }, "postRequestScript": "companyAccountBackupStatus.js" }, - "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, "metadata": [ { "name": "accountName", "displayName": "Account", "shape": "string", "role": "label" }, { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, + { + "name": "overallStatus", + "displayName": "Status", + "computed": true, + "valueExpression": "{{ $['status'] === 1 ? 'success' : ($['status'] === 2 ? 'warning' : 'error') }}", + "shape": "state" + }, { "name": "status", "displayName": "Status Code", "shape": "number", - "help": "Raw Redstor status code — RedAPI's public documentation does not define the meaning of each value." + "visible": false, + "help": "Raw Redstor status code: 0 = Failed, 1 = Completed successfully, 2 = Completed with warnings, 3 = Completed with errors." }, { "name": "message", "displayName": "Message", "shape": "string" }, { "name": "errorCount", "displayName": "Error Count", "shape": "number" }, diff --git a/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json b/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json index 4e82056a..e58472f5 100644 --- a/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json +++ b/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json @@ -16,7 +16,7 @@ }, "pathToData": "results" }, - "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, "metadata": [ { "name": "accountName", "displayName": "Account", "shape": "string", "role": "label" }, { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, diff --git a/plugins/Redstor/v1/dataStreams/companyBackupStatus.json b/plugins/Redstor/v1/dataStreams/companyBackupStatus.json index 581482c0..6023689d 100644 --- a/plugins/Redstor/v1/dataStreams/companyBackupStatus.json +++ b/plugins/Redstor/v1/dataStreams/companyBackupStatus.json @@ -16,7 +16,7 @@ }, "pathToData": "results" }, - "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, "metadata": [ { "name": "productName", "displayName": "Product", "shape": "string", "role": "label" }, { "name": "succeeded", "displayName": "Succeeded", "shape": "number" }, diff --git a/plugins/Redstor/v1/dataStreams/companyConsumption.json b/plugins/Redstor/v1/dataStreams/companyConsumption.json index 9863a1da..a39a38d8 100644 --- a/plugins/Redstor/v1/dataStreams/companyConsumption.json +++ b/plugins/Redstor/v1/dataStreams/companyConsumption.json @@ -2,7 +2,7 @@ "name": "companyConsumption", "displayName": "Company Consumption", "description": "Storage consumption and seat usage by product for a company", - "tags": ["Consumption", "Company"], + "tags": ["Usage", "Company"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", @@ -19,13 +19,13 @@ }, "pathToData": "results" }, - "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, "ui": [ { "type": "checkbox", "name": "excludeTrials", "label": "Exclude trial subscriptions", - "value": "true" + "defaultValue": true } ], "metadata": [ diff --git a/plugins/Redstor/v1/dataStreams/companyRestoreStatus.json b/plugins/Redstor/v1/dataStreams/companyRestoreStatus.json index 961e5313..a9dedf5d 100644 --- a/plugins/Redstor/v1/dataStreams/companyRestoreStatus.json +++ b/plugins/Redstor/v1/dataStreams/companyRestoreStatus.json @@ -16,7 +16,7 @@ }, "pathToData": "results" }, - "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, "metadata": [ { "name": "productName", "displayName": "Product", "shape": "string", "role": "label" }, { "name": "succeeded", "displayName": "Succeeded", "shape": "number" }, diff --git a/plugins/Redstor/v1/dataStreams/companySubscriptions.json b/plugins/Redstor/v1/dataStreams/companySubscriptions.json index 3a805d11..75902633 100644 --- a/plugins/Redstor/v1/dataStreams/companySubscriptions.json +++ b/plugins/Redstor/v1/dataStreams/companySubscriptions.json @@ -16,7 +16,7 @@ }, "pathToData": "results" }, - "matches": { "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, "metadata": [ { "name": "subscriptionId", "displayName": "Subscription ID", "shape": "guid", "role": "label" }, { "name": "productId", "displayName": "Product ID", "shape": "number" }, diff --git a/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json index 88c4b9fa..7c7ad03f 100644 --- a/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json +++ b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json @@ -1,6 +1,7 @@ { "name": "Account", "schemaVersion": "1.5", + "timeframe": "none", "variables": ["{{variables.[Account]}}"], "dashboard": { "_type": "layout/grid", diff --git a/plugins/Redstor/v1/defaultContent/companyPerspective.dash.json b/plugins/Redstor/v1/defaultContent/companyPerspective.dash.json deleted file mode 100644 index 591bfc76..00000000 --- a/plugins/Redstor/v1/defaultContent/companyPerspective.dash.json +++ /dev/null @@ -1,376 +0,0 @@ -{ - "name": "Company", - "schemaVersion": "1.5", - "variables": ["{{variables.[Company]}}"], - "dashboard": { - "_type": "layout/grid", - "columns": 4, - "version": 1, - "contents": [ - { - "i": "986d50b9-378e-4a2e-ba18-7cce363c75d3", - "x": 0, - "y": 0, - "w": 1, - "h": 2, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Accounts", - "description": "", - "activePluginConfigIds": ["{{configId}}"], - "timeframe": "none", - "variables": ["{{variables.[Company]}}"], - "dataStream": { - "id": "{{dataStreams.accounts}}", - "name": "accounts", - "pluginConfigId": "{{configId}}", - "group": { "by": [], "aggregate": [{ "type": "count" }] } - }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, - "visualisation": { - "type": "data-stream-scalar", - "config": { - "data-stream-scalar": { - "value": "count", - "comparisonColumn": "none" - } - } - } - } - }, - { - "i": "3918894f-baa9-4f10-be3b-f5e5a53e76c9", - "x": 1, - "y": 0, - "w": 1, - "h": 2, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Backup Failures", - "description": "", - "activePluginConfigIds": ["{{configId}}"], - "timeframe": "none", - "variables": ["{{variables.[Company]}}"], - "dataStream": { - "id": "{{dataStreams.companyBackupStatus}}", - "name": "companyBackupStatus", - "pluginConfigId": "{{configId}}", - "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["failed"] }] } - }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, - "visualisation": { - "type": "data-stream-scalar", - "config": { - "data-stream-scalar": { - "value": "failed_sum", - "comparisonColumn": "none" - } - } - } - } - }, - { - "i": "8750e03d-1ec0-4391-a7d8-62982b90831a", - "x": 2, - "y": 0, - "w": 1, - "h": 2, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Restore Failures", - "description": "", - "activePluginConfigIds": ["{{configId}}"], - "timeframe": "none", - "variables": ["{{variables.[Company]}}"], - "dataStream": { - "id": "{{dataStreams.companyRestoreStatus}}", - "name": "companyRestoreStatus", - "pluginConfigId": "{{configId}}", - "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["failed"] }] } - }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, - "visualisation": { - "type": "data-stream-scalar", - "config": { - "data-stream-scalar": { - "value": "failed_sum", - "comparisonColumn": "none" - } - } - } - } - }, - { - "i": "f5cc2719-71c2-465c-993f-5894dc6b5423", - "x": 3, - "y": 0, - "w": 1, - "h": 2, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Data Protected", - "description": "", - "activePluginConfigIds": ["{{configId}}"], - "timeframe": "none", - "variables": ["{{variables.[Company]}}"], - "dataStream": { - "id": "{{dataStreams.companyConsumption}}", - "name": "companyConsumption", - "pluginConfigId": "{{configId}}", - "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["totalDataProtectedInBytes"] }] } - }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, - "visualisation": { - "type": "data-stream-scalar", - "config": { - "data-stream-scalar": { - "value": "totalDataProtectedInBytes_sum", - "comparisonColumn": "none" - } - } - } - } - }, - { - "i": "cc472807-ff58-4d5c-8b69-ce6dedc36618", - "x": 0, - "y": 2, - "w": 2, - "h": 4, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Backup Status", - "description": "", - "activePluginConfigIds": ["{{configId}}"], - "timeframe": "none", - "variables": ["{{variables.[Company]}}"], - "dataStream": { - "id": "{{dataStreams.companyBackupStatus}}", - "name": "companyBackupStatus", - "pluginConfigId": "{{configId}}" - }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": [ - "productName", - "overallStatus", - "succeeded", - "completedWithWarnings", - "completedWithErrors", - "failed", - "missed" - ] - } - } - } - } - }, - { - "i": "937fbbe2-5d23-400f-b307-af2c068cfa5f", - "x": 2, - "y": 2, - "w": 2, - "h": 4, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Restore Status", - "description": "", - "activePluginConfigIds": ["{{configId}}"], - "timeframe": "none", - "variables": ["{{variables.[Company]}}"], - "dataStream": { - "id": "{{dataStreams.companyRestoreStatus}}", - "name": "companyRestoreStatus", - "pluginConfigId": "{{configId}}" - }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": [ - "productName", - "overallStatus", - "succeeded", - "completedWithWarnings", - "completedWithErrors", - "failed" - ] - } - } - } - } - }, - { - "i": "f0405981-4395-44a5-89cd-5fb290f6976e", - "x": 0, - "y": 6, - "w": 2, - "h": 4, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Consumption", - "description": "", - "activePluginConfigIds": ["{{configId}}"], - "timeframe": "none", - "variables": ["{{variables.[Company]}}"], - "dataStream": { - "id": "{{dataStreams.companyConsumption}}", - "name": "companyConsumption", - "pluginConfigId": "{{configId}}" - }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": [ - "productName", - "totalDataProtectedInBytes", - "activeSeats", - "inactiveSeats", - "sharedSeats", - "activeWorkloads", - "licensedUsers", - "isTrial", - "sku" - ], - "hiddenColumns": ["totalDataProtectedInBytesExternal", "fupOverageInBytes"] - } - } - } - } - }, - { - "i": "7c6937ea-9ef8-44f4-bf6a-596b8f7615bb", - "x": 2, - "y": 6, - "w": 2, - "h": 4, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Subscriptions", - "description": "", - "activePluginConfigIds": ["{{configId}}"], - "timeframe": "none", - "variables": ["{{variables.[Company]}}"], - "dataStream": { - "id": "{{dataStreams.companySubscriptions}}", - "name": "companySubscriptions", - "pluginConfigId": "{{configId}}" - }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": ["productId", "editionId", "trialStatus", "isActive", "subscriptionId"] - } - } - } - } - }, - { - "i": "9de4626d-39c3-408e-b008-248ee2cc737a", - "x": 0, - "y": 10, - "w": 4, - "h": 4, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Accounts", - "description": "", - "activePluginConfigIds": ["{{configId}}"], - "timeframe": "none", - "variables": ["{{variables.[Company]}}"], - "dataStream": { - "id": "{{dataStreams.accounts}}", - "name": "accounts", - "pluginConfigId": "{{configId}}" - }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": ["accountName", "serviceName", "productName", "storageRegion", "createOn"], - "hiddenColumns": ["displayName"] - } - } - } - } - } - ] - } -} diff --git a/plugins/Redstor/v1/defaultContent/manifest.json b/plugins/Redstor/v1/defaultContent/manifest.json index f0077666..85c531a0 100644 --- a/plugins/Redstor/v1/defaultContent/manifest.json +++ b/plugins/Redstor/v1/defaultContent/manifest.json @@ -1,7 +1,6 @@ { "items": [ { "name": "overview", "type": "dashboard" }, - { "name": "companyPerspective", "type": "dashboard" }, { "name": "accountPerspective", "type": "dashboard" } ] } diff --git a/plugins/Redstor/v1/defaultContent/overview.dash.json b/plugins/Redstor/v1/defaultContent/overview.dash.json index 94ba7b11..76c36c09 100644 --- a/plugins/Redstor/v1/defaultContent/overview.dash.json +++ b/plugins/Redstor/v1/defaultContent/overview.dash.json @@ -1,6 +1,7 @@ { "name": "Overview", "schemaVersion": "1.5", + "timeframe": "none", "variables": ["{{variables.[Company]}}"], "dashboard": { "_type": "layout/grid", @@ -186,19 +187,21 @@ "variable": "{{variables.[Company]}}" }, "visualisation": { - "type": "data-stream-table", + "type": "data-stream-bar-chart", "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": [ - "productName", - "overallStatus", - "succeeded", - "completedWithWarnings", - "completedWithErrors", - "failed", - "missed" - ] + "data-stream-bar-chart": { + "xAxisData": "productName", + "yAxisData": ["succeeded", "completedWithWarnings", "completedWithErrors", "failed", "missed"], + "xAxisGroup": "none", + "horizontalLayout": "vertical", + "grouping": false, + "displayMode": "percentage", + "showLegend": true, + "legendPosition": "bottom", + "showGrid": true, + "showValue": false, + "showTotals": false, + "range": { "type": "auto" } } } } @@ -231,18 +234,21 @@ "variable": "{{variables.[Company]}}" }, "visualisation": { - "type": "data-stream-table", + "type": "data-stream-bar-chart", "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": [ - "productName", - "overallStatus", - "succeeded", - "completedWithWarnings", - "completedWithErrors", - "failed" - ] + "data-stream-bar-chart": { + "xAxisData": "productName", + "yAxisData": ["succeeded", "completedWithWarnings", "completedWithErrors", "failed"], + "xAxisGroup": "none", + "horizontalLayout": "vertical", + "grouping": false, + "displayMode": "percentage", + "showLegend": true, + "legendPosition": "bottom", + "showGrid": true, + "showValue": false, + "showTotals": false, + "range": { "type": "auto" } } } } diff --git a/plugins/Redstor/v1/defaultContent/scopes.json b/plugins/Redstor/v1/defaultContent/scopes.json index ba7615e6..689f70d0 100644 --- a/plugins/Redstor/v1/defaultContent/scopes.json +++ b/plugins/Redstor/v1/defaultContent/scopes.json @@ -2,7 +2,7 @@ { "name": "Companies", "matches": { - "sourceType": { "type": "oneOf", "values": ["Redstor Company"] } + "sourceType": { "type": "oneOf", "values": ["Company"] } }, "variable": { "name": "Company", @@ -14,7 +14,7 @@ { "name": "Accounts", "matches": { - "sourceType": { "type": "oneOf", "values": ["Redstor Account"] } + "sourceType": { "type": "oneOf", "values": ["Account"] } }, "variable": { "name": "Account", diff --git a/plugins/Redstor/v1/docs/README.md b/plugins/Redstor/v1/docs/README.md index 51d377ef..7ca51b33 100644 --- a/plugins/Redstor/v1/docs/README.md +++ b/plugins/Redstor/v1/docs/README.md @@ -31,7 +31,7 @@ On save, the plugin authenticates by exchanging a signed JWT for a Redstor acces - **Consumption & subscriptions** — storage and seat usage, and active product subscriptions, per company. - **Account errors** — backup and restore error and warning messages for a specific account. -The out-of-the-box dashboards include a Partner-wide **Overview** plus a perspective for each **Company** and **Account**. +The out-of-the-box dashboards include a Company **Overview** and a perspective for each **Account**. ## Data streams @@ -49,16 +49,16 @@ The out-of-the-box dashboards include a Partner-wide **Overview** plus a perspec | Object type | API source | Represents | | ----------- | ---------- | ---------- | -| **Redstor Company** | `GET /companies/{companyId}`, `GET /companies/{companyId}/customers` | Your configured Partner company and its direct customer companies. | -| **Redstor Account** | `GET /storage/accounts` | A backup or storage account belonging to a company, for one product/service. | +| **Company** | `GET /companies/{companyId}`, `GET /companies/{companyId}/customers` | Your configured Partner company and its direct customer companies. | +| **Account** | `GET /storage/accounts` | A backup or storage account belonging to a company, for one product/service. | **Relationships:** each Account belongs to its parent Company. ## Known limitations -- **Entirely unverified against a live Redstor tenant** — this plugin was authored directly from Redstor's public RedAPI OpenAPI specification, with no Redstor Partner Admin credentials available to test authentication or any data stream against a real account. Verify against a live tenant before relying on it in production. +- **Unverified against a live Redstor tenant** — this plugin's endpoints, parameters, and response fields were checked field-by-field against Redstor's public RedAPI OpenAPI specification (`https://assets.redstor.com/api-gateway/swagger-ui/public.json`), but no Redstor Partner Admin credentials were available to test authentication or any data stream against a real account. Verify against a live tenant before relying on it in production. - **No historical or time-range data** — RedAPI exposes no `from`/`to` range parameter on any endpoint; every stream returns a current-state snapshot, and dashboards have no timeframe picker. -- **Backup/restore status codes are undocumented** — the numeric `status` values in Company Account Backup/Restore Status aren't defined anywhere in RedAPI's public documentation, so they're shown as raw numbers rather than mapped to a health color. +- **Restore status codes are undocumented** — the numeric `status` values in Company Account Restore Status aren't defined anywhere in RedAPI's public documentation, so they're shown as a raw number rather than mapped to a health color. (Company Account Backup Status's codes *are* documented and are mapped to a status color.) - **Subscriptions show IDs, not names** — RedAPI's `/subscriptions` endpoint returns `productId`/`editionId` only, with no product name lookup; this plugin doesn't index a Products type, so subscription rows show numeric IDs. - **Company hierarchy is one level deep** — only the configured Partner company and its direct customers are indexed; deeper reseller-of-reseller chains aren't walked recursively. - **Private key format assumed PEM** — the exact format RedApp downloads for a RedAPI service account key couldn't be confirmed without live access; PEM is assumed based on standard `private_key_jwt` practice. diff --git a/plugins/Redstor/v1/indexDefinitions/default.json b/plugins/Redstor/v1/indexDefinitions/default.json index 59021449..73f9e362 100644 --- a/plugins/Redstor/v1/indexDefinitions/default.json +++ b/plugins/Redstor/v1/indexDefinitions/default.json @@ -7,7 +7,7 @@ "objectMapping": { "id": "companyId", "name": "companyName", - "type": { "value": "Redstor Company" }, + "type": { "value": "Company" }, "properties": ["type", "isEnabled", "parentCompanyId"] } }, @@ -18,7 +18,7 @@ "objectMapping": { "id": "companyId", "name": "companyName", - "type": { "value": "Redstor Company" }, + "type": { "value": "Company" }, "properties": ["type", "isEnabled", "parentCompanyId"] } }, @@ -26,13 +26,13 @@ "name": "accounts", "dataStream": { "name": "accounts" }, "scope": { - "query": "g.V().has(\"sourceType\", \"Redstor Company\")" + "query": "g.V().has(\"sourceType\", \"Company\")" }, "timeframe": "none", "objectMapping": { "id": "accountId", "name": "accountName", - "type": { "value": "Redstor Account" }, + "type": { "value": "Account" }, "properties": [ "displayName", "serviceId", diff --git a/plugins/Redstor/v1/metadata.json b/plugins/Redstor/v1/metadata.json index a16c1718..4f4c6522 100644 --- a/plugins/Redstor/v1/metadata.json +++ b/plugins/Redstor/v1/metadata.json @@ -10,7 +10,7 @@ "importNotSupported": false, "restrictedToPlatforms": [], "keywords": ["backup", "disaster recovery", "archiving", "msp", "redstor", "redapi"], - "objectTypes": ["Redstor Company", "Redstor Account"], + "objectTypes": ["Company", "Account"], "links": [ { "category": "documentation", diff --git a/plugins/Redstor/v1/ui.json b/plugins/Redstor/v1/ui.json index 817eda60..58b4dbb3 100644 --- a/plugins/Redstor/v1/ui.json +++ b/plugins/Redstor/v1/ui.json @@ -11,6 +11,7 @@ "type": "text", "name": "clientId", "label": "Client ID", + "placeholder": "Enter the RedAPI service account Client ID", "help": "The Client ID of the RedAPI service account, created in RedApp → **RedAPI → Service accounts**.", "validation": { "required": true } }, @@ -19,6 +20,7 @@ "name": "privateKey", "label": "Private Key", "rows": 12, + "allowEncryption": true, "help": "The PEM-encoded private key downloaded when you created the service account's key in RedApp → **RedAPI → Service accounts**. Keep this secret — it authenticates as your Partner Admin. Paste the full key including the `-----BEGIN PRIVATE KEY-----` / `-----END PRIVATE KEY-----` lines.", "validation": { "required": true } } From 13e1e9d3f750826ec76f06a9d0f5617d57365357 Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Mon, 7 Sep 2026 18:05:08 +0100 Subject: [PATCH 03/24] Fix Redstor auth to use JWK signing and Bearer token RedApp issues service account keys as JWK, not PEM, and the deployed API requires Authorization: Bearer rather than the X-Api-Key scheme documented in RedAPI's OpenAPI spec - confirmed against a live account. Also renames the companyId config field to entityIdentifier and adds the required apiVersion query arg. Co-Authored-By: Claude Sonnet 5 --- .../v1/dataStreams/companyCustomers.json | 2 +- .../v1/dataStreams/companyProfile.json | 2 +- plugins/Redstor/v1/docs/README.md | 9 ++- plugins/Redstor/v1/metadata.json | 2 +- plugins/Redstor/v1/preRequest.js | 59 +++++++++++-------- plugins/Redstor/v1/ui.json | 6 +- 6 files changed, 46 insertions(+), 34 deletions(-) diff --git a/plugins/Redstor/v1/dataStreams/companyCustomers.json b/plugins/Redstor/v1/dataStreams/companyCustomers.json index 066799bb..65e48ea0 100644 --- a/plugins/Redstor/v1/dataStreams/companyCustomers.json +++ b/plugins/Redstor/v1/dataStreams/companyCustomers.json @@ -6,7 +6,7 @@ "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", - "endpointPath": "/companies/{{dataSource.companyId}}/customers", + "endpointPath": "/companies/{{dataSource.entityIdentifier}}/customers", "paging": { "mode": "token", "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, diff --git a/plugins/Redstor/v1/dataStreams/companyProfile.json b/plugins/Redstor/v1/dataStreams/companyProfile.json index ce6aef0d..3767639a 100644 --- a/plugins/Redstor/v1/dataStreams/companyProfile.json +++ b/plugins/Redstor/v1/dataStreams/companyProfile.json @@ -6,7 +6,7 @@ "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", - "endpointPath": "/companies/{{dataSource.companyId}}", + "endpointPath": "/companies/{{dataSource.entityIdentifier}}", "postRequestScript": "companyProfile.js" }, "matches": "none", diff --git a/plugins/Redstor/v1/docs/README.md b/plugins/Redstor/v1/docs/README.md index 7ca51b33..68993bf6 100644 --- a/plugins/Redstor/v1/docs/README.md +++ b/plugins/Redstor/v1/docs/README.md @@ -9,9 +9,9 @@ You will need a RedAPI **service account** with its Client ID and private key, p 1. Sign in to [RedApp](https://redapp.redstor.com) as a **Partner Admin**. 2. Note your Partner company's ID — find it under **Company Settings** — and paste it into the **Company ID** field. 3. Go to **RedAPI → Service accounts** and click to add a new service account. Give it a descriptive name and assign it access to your Partner company (and the customers you want visibility into). -4. Under the service account, create a **key**. Redstor generates a Client ID and a private/public key pair — download the private key file. +4. Under the service account, create a **key**. Redstor generates a Client ID and a JSON Web Key (JWK) file — download the JWK file. 5. Copy the **Client ID** into the **Client ID** field. -6. Open the downloaded private key file and paste its full contents — including the `-----BEGIN PRIVATE KEY-----` / `-----END PRIVATE KEY-----` lines — into the **Private Key** field. +6. Open the downloaded JWK file (a `.json` file) and paste its full contents, exactly as downloaded, into the **Private Key** field. ## Configuration fields @@ -19,7 +19,7 @@ You will need a RedAPI **service account** with its Client ID and private key, p | ----- | ---------- | ---------------- | -------- | | **Company ID** | Your Redstor Partner company's ID. Scopes every API call to this company and its direct customers. | RedApp → **Company Settings**. | Yes | | **Client ID** | Identifies the RedAPI service account used to authenticate. | RedApp → **RedAPI → Service accounts** → your service account's key. | Yes | -| **Private Key** | The PEM-encoded private key paired with the Client ID; signs the client assertion used to obtain access tokens. | Downloaded when the service account's key was created. | Yes | +| **Private Key** | The JSON Web Key (JWK) paired with the Client ID; signs the client assertion used to obtain access tokens. | Downloaded when the service account's key was created. | Yes | On save, the plugin authenticates by exchanging a signed JWT for a Redstor access token and calling your company's profile; an invalid Company ID, Client ID, or Private Key fails setup with an authentication error. @@ -56,11 +56,10 @@ The out-of-the-box dashboards include a Company **Overview** and a perspective f ## Known limitations -- **Unverified against a live Redstor tenant** — this plugin's endpoints, parameters, and response fields were checked field-by-field against Redstor's public RedAPI OpenAPI specification (`https://assets.redstor.com/api-gateway/swagger-ui/public.json`), but no Redstor Partner Admin credentials were available to test authentication or any data stream against a real account. Verify against a live tenant before relying on it in production. +- **Only partially verified against a live Redstor tenant** — authentication (the JWK-signed `private_key_jwt` exchange) and the Company Profile endpoint have been confirmed working end-to-end against a real Partner account. Every other data stream was still only checked field-by-field against RedAPI's public OpenAPI specification (`https://assets.redstor.com/api-gateway/swagger-ui/public.json`), which is not fully reliable — the spec's documented auth header (`X-Api-Key`) turned out to be wrong; the live API actually requires a standard `Authorization: Bearer` header plus an `apiVersion=2.0` query parameter the spec lists as optional. Verify the remaining data streams against a live tenant before relying on them in production. - **No historical or time-range data** — RedAPI exposes no `from`/`to` range parameter on any endpoint; every stream returns a current-state snapshot, and dashboards have no timeframe picker. - **Restore status codes are undocumented** — the numeric `status` values in Company Account Restore Status aren't defined anywhere in RedAPI's public documentation, so they're shown as a raw number rather than mapped to a health color. (Company Account Backup Status's codes *are* documented and are mapped to a status color.) - **Subscriptions show IDs, not names** — RedAPI's `/subscriptions` endpoint returns `productId`/`editionId` only, with no product name lookup; this plugin doesn't index a Products type, so subscription rows show numeric IDs. - **Company hierarchy is one level deep** — only the configured Partner company and its direct customers are indexed; deeper reseller-of-reseller chains aren't walked recursively. -- **Private key format assumed PEM** — the exact format RedApp downloads for a RedAPI service account key couldn't be confirmed without live access; PEM is assumed based on standard `private_key_jwt` practice. - **Subject to Redstor's fair-use throttling** — RedAPI enforces rate limiting; specific limits aren't published. - **Read-only** — the plugin never creates, modifies, or deletes anything in Redstor. diff --git a/plugins/Redstor/v1/metadata.json b/plugins/Redstor/v1/metadata.json index 4f4c6522..f1b840b5 100644 --- a/plugins/Redstor/v1/metadata.json +++ b/plugins/Redstor/v1/metadata.json @@ -30,7 +30,7 @@ "baseUrl": "https://api.redstor.com", "authMode": "none", "headers": [{ "key": "Accept", "value": "application/json" }], - "queryArgs": [], + "queryArgs": [{ "key": "apiVersion", "value": "2.0" }], "scriptingVariables": [ { "key": "clientId", "value": "{{clientId}}" }, { "key": "privateKey", "value": "{{privateKey}}" } diff --git a/plugins/Redstor/v1/preRequest.js b/plugins/Redstor/v1/preRequest.js index 285a380a..69794c4c 100644 --- a/plugins/Redstor/v1/preRequest.js +++ b/plugins/Redstor/v1/preRequest.js @@ -3,9 +3,17 @@ // assertion is signed with a service account's private key and exchanged // at the token endpoint for a short-lived access token. // -// Unlike a standard OAuth2 flow, the resulting access token is NOT sent as -// `Authorization: Bearer` on API calls — RedAPI's OpenAPI spec declares the -// security scheme as an `X-Api-Key` header, so that's where the token goes. +// RedApp issues the service account's signing key as a JSON Web Key (JWK), +// not a PEM file — confirmed against a live RedApp export. The JWK is +// imported directly via crypto.subtle, and its `kid` (when present) is +// echoed in the JWT header, since RedAPI's auth server needs it to identify +// which registered key signed the assertion. +// +// The access token IS sent as a standard `Authorization: Bearer` header — +// confirmed against a live account. RedAPI's published OpenAPI spec +// declares the security scheme as an `X-Api-Key` header instead, but that +// does not match how the deployed API actually behaves; sending X-Api-Key +// gets a 401 with `WWW-Authenticate: Bearer`. // // The access token is cached in `state` and reused until shortly before // expiration; the client-assertion JWT itself is re-signed on every @@ -15,19 +23,6 @@ const TOKEN_ENDPOINT = "https://id.redstor.com/connect/token"; // --- helpers ----------------------------------------------------------------------- -function pemToArrayBuffer(pem) { - const base64 = String(pem) - .replace(/-----BEGIN [^-]+-----/, "") - .replace(/-----END [^-]+-----/, "") - .replace(/\s+/g, ""); - const binary = atob(base64); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - return bytes.buffer; -} - function bytesToBase64Url(bytes) { let binary = ""; for (let i = 0; i < bytes.length; i++) { @@ -40,8 +35,14 @@ function utf8ToBytes(str) { return new TextEncoder().encode(str); } -async function signClientAssertion(clientId, privateKeyPem) { +async function signClientAssertion(clientId, privateKeyJwk) { const header = { alg: "RS256", typ: "JWT" }; + if (privateKeyJwk.kid) { + // RedAPI's auth server expects the kid from the issued JWK so it can + // look up the matching registered public key for this client. + header.kid = privateKeyJwk.kid; + } + const nowSeconds = Math.floor(Date.now() / 1000); const payload = { iss: clientId, @@ -59,8 +60,8 @@ async function signClientAssertion(clientId, privateKeyPem) { const signingInput = `${encodedHeader}.${encodedPayload}`; const key = await crypto.subtle.importKey( - "pkcs8", - pemToArrayBuffer(privateKeyPem), + "jwk", + privateKeyJwk, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign"], @@ -79,19 +80,29 @@ async function signClientAssertion(clientId, privateKeyPem) { if (typeof state?.token !== "string" || (state?.expiryTime ?? 0) <= Date.now()) { const clientId = String(secrets.clientId || "").trim(); - const privateKey = String(secrets.privateKey || "").trim(); + const privateKeyRaw = String(secrets.privateKey || "").trim(); - if (!clientId || !privateKey) { + if (!clientId || !privateKeyRaw) { api.report.error("Redstor RedAPI Client ID or Private Key is not configured."); return; } + let privateKeyJwk; + try { + privateKeyJwk = JSON.parse(privateKeyRaw); + } catch (e) { + api.report.error( + "Could not parse the Redstor Private Key — it must be the full JSON Web Key (JWK) downloaded from RedApp, pasted as-is.", + ); + return; + } + let clientAssertion; try { - clientAssertion = await signClientAssertion(clientId, privateKey); + clientAssertion = await signClientAssertion(clientId, privateKeyJwk); } catch (e) { api.report.error( - `Could not sign the Redstor client assertion — check the Private Key is a valid PEM-encoded RSA private key. ${e}`, + `Could not sign the Redstor client assertion — check the Private Key is a valid RSA JWK. ${e}`, ); return; } @@ -147,4 +158,4 @@ if (typeof state?.token !== "string" || (state?.expiryTime ?? 0) <= Date.now()) // --- request --------------------------------------------------------------------- -headers["X-Api-Key"] = state.token; +headers["Authorization"] = `Bearer ${state.token}`; diff --git a/plugins/Redstor/v1/ui.json b/plugins/Redstor/v1/ui.json index 58b4dbb3..f9fabc82 100644 --- a/plugins/Redstor/v1/ui.json +++ b/plugins/Redstor/v1/ui.json @@ -1,7 +1,7 @@ [ { "type": "text", - "name": "companyId", + "name": "entityIdentifier", "label": "Company ID", "placeholder": "e.g. 3fa85f64-5717-4562-b3fc-2c963f66afa6", "help": "Your Redstor **Partner** company's ID in RedApp — found under **RedApp → Company Settings**. Every request is scoped to this company and its direct customers.", @@ -11,6 +11,7 @@ "type": "text", "name": "clientId", "label": "Client ID", + "allowEncryption": true, "placeholder": "Enter the RedAPI service account Client ID", "help": "The Client ID of the RedAPI service account, created in RedApp → **RedAPI → Service accounts**.", "validation": { "required": true } @@ -21,7 +22,8 @@ "label": "Private Key", "rows": 12, "allowEncryption": true, - "help": "The PEM-encoded private key downloaded when you created the service account's key in RedApp → **RedAPI → Service accounts**. Keep this secret — it authenticates as your Partner Admin. Paste the full key including the `-----BEGIN PRIVATE KEY-----` / `-----END PRIVATE KEY-----` lines.", + "placeholder": "Paste the full JWK JSON downloaded from RedApp", + "help": "The JSON Web Key (JWK) downloaded when you created the service account's key in RedApp → **RedAPI → Service accounts**. Keep this secret — it authenticates as your Partner Admin. Paste the full JSON file contents, exactly as downloaded.", "validation": { "required": true } } ] From e12936e38fd91b2a9163e8b06f5966f2ca5dbd15 Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Tue, 8 Sep 2026 22:17:35 +0100 Subject: [PATCH 04/24] Fix Redstor data model issues found in live tenant testing Verified every data stream against a live Partner tenant for the first time, which surfaced problems that checking field-by-field against the OpenAPI spec had missed. - Separate seats from backup accounts. Redstor bills per seat (one person), but /storage/accounts returns a row per workload, so a Microsoft 365 seat appears as both an Exchange and a OneDrive account. The test company had 856 accounts against 363 seats, so the Accounts tile overstated seat usage 2.4x. Renamed the stream to Backup Accounts and documented the distinction - Cut Company Account Backup Status from 3976 rows (4.2MB against a 6MB response cap) to 568 by returning the latest run per account, with a toggle for the full seven day history - Index Products, Services and Editions so subscriptions and per-account status show names rather than numeric IDs. Editions are keyed on a composite product-edition key because Redstor reuses edition ID 5 across every product, and the key is built in a script since object lookups ignore computed columns - Explain the Account Backup/Restore Errors 422 instead of surfacing it raw. The errors endpoint only recognises accounts with backup history, which excluded a third of the test company's accounts - Correct the service ID mislabel. /backups/status/accounts and /restores/status/accounts return service IDs in a field named productId, which would have mis-resolved against Product objects - Remove the companyId column, whose expression failed on every row and was being indexed into the graph as an error string - Mask the Client ID field, and rewrite the README for a user audience rather than as a record of implementation decisions Co-Authored-By: Claude Sonnet 5 --- plugins/Redstor/v1/custom_types.json | 21 +++++++ .../v1/dataStreams/accountBackupErrors.json | 3 +- .../v1/dataStreams/accountRestoreErrors.json | 3 +- .../{accounts.json => backupAccounts.json} | 16 ++--- .../companyAccountBackupStatus.json | 20 +++++- .../companyAccountRestoreStatus.json | 15 ++++- .../v1/dataStreams/companySubscriptions.json | 24 ++++++-- .../v1/dataStreams/productEditions.json | 28 +++++++++ .../v1/dataStreams/productServices.json | 26 ++++++++ plugins/Redstor/v1/dataStreams/products.json | 24 ++++++++ .../scripts/companyAccountBackupStatus.js | 22 ++++--- .../scripts/companySubscriptions.js | 10 +++ .../errorHandling/accountBackupErrors.js | 7 +++ .../errorHandling/accountRestoreErrors.js | 6 ++ .../v1/dataStreams/scripts/productEditions.js | 15 +++++ .../v1/dataStreams/scripts/productServices.js | 11 ++++ .../v1/defaultContent/overview.dash.json | 14 ++--- plugins/Redstor/v1/docs/README.md | 61 +++++++++++-------- .../Redstor/v1/indexDefinitions/default.json | 39 ++++++++++-- plugins/Redstor/v1/metadata.json | 2 +- plugins/Redstor/v1/ui.json | 2 +- 21 files changed, 304 insertions(+), 65 deletions(-) rename plugins/Redstor/v1/dataStreams/{accounts.json => backupAccounts.json} (79%) create mode 100644 plugins/Redstor/v1/dataStreams/productEditions.json create mode 100644 plugins/Redstor/v1/dataStreams/productServices.json create mode 100644 plugins/Redstor/v1/dataStreams/products.json create mode 100644 plugins/Redstor/v1/dataStreams/scripts/companySubscriptions.js create mode 100644 plugins/Redstor/v1/dataStreams/scripts/errorHandling/accountBackupErrors.js create mode 100644 plugins/Redstor/v1/dataStreams/scripts/errorHandling/accountRestoreErrors.js create mode 100644 plugins/Redstor/v1/dataStreams/scripts/productEditions.js create mode 100644 plugins/Redstor/v1/dataStreams/scripts/productServices.js diff --git a/plugins/Redstor/v1/custom_types.json b/plugins/Redstor/v1/custom_types.json index b3d0a061..4d2ec72c 100644 --- a/plugins/Redstor/v1/custom_types.json +++ b/plugins/Redstor/v1/custom_types.json @@ -12,5 +12,26 @@ "icon": "cloud-arrow-up", "singular": "Account", "plural": "Accounts" + }, + { + "name": "Redstor Product", + "sourceType": "Product", + "icon": "cube", + "singular": "Product", + "plural": "Products" + }, + { + "name": "Redstor Service", + "sourceType": "Service", + "icon": "sitemap", + "singular": "Service", + "plural": "Services" + }, + { + "name": "Redstor Edition", + "sourceType": "Edition", + "icon": "layer-group", + "singular": "Edition", + "plural": "Editions" } ] diff --git a/plugins/Redstor/v1/dataStreams/accountBackupErrors.json b/plugins/Redstor/v1/dataStreams/accountBackupErrors.json index 65cc48ce..23d09998 100644 --- a/plugins/Redstor/v1/dataStreams/accountBackupErrors.json +++ b/plugins/Redstor/v1/dataStreams/accountBackupErrors.json @@ -14,7 +14,8 @@ "in": { "realm": "payload", "path": "nextPageToken" }, "out": { "realm": "queryArg", "path": "pageToken" } }, - "pathToData": "results" + "pathToData": "results", + "errorHandling": { "type": "script", "script": "errorHandling/accountBackupErrors.js" } }, "matches": { "sourceType": { "type": "oneOf", "values": ["Account"] } }, "metadata": [ diff --git a/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json b/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json index 43bf958e..940b9377 100644 --- a/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json +++ b/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json @@ -14,7 +14,8 @@ "in": { "realm": "payload", "path": "nextPageToken" }, "out": { "realm": "queryArg", "path": "pageToken" } }, - "pathToData": "results" + "pathToData": "results", + "errorHandling": { "type": "script", "script": "errorHandling/accountRestoreErrors.js" } }, "matches": { "sourceType": { "type": "oneOf", "values": ["Account"] } }, "metadata": [ diff --git a/plugins/Redstor/v1/dataStreams/accounts.json b/plugins/Redstor/v1/dataStreams/backupAccounts.json similarity index 79% rename from plugins/Redstor/v1/dataStreams/accounts.json rename to plugins/Redstor/v1/dataStreams/backupAccounts.json index 4e34cff0..b0757546 100644 --- a/plugins/Redstor/v1/dataStreams/accounts.json +++ b/plugins/Redstor/v1/dataStreams/backupAccounts.json @@ -1,7 +1,7 @@ { - "name": "accounts", - "displayName": "Company Accounts", - "description": "Backup and storage accounts for a Redstor company", + "name": "backupAccounts", + "displayName": "Backup Accounts", + "description": "Backup accounts for a company (one row per account, and a single seat can hold more than one)", "tags": ["Account"], "baseDataSourceName": "httpRequestScopedSingle", "config": { @@ -26,15 +26,7 @@ { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, { "name": "productName", "displayName": "Product", "shape": "string" }, { "name": "storageRegion", "displayName": "Storage Region", "shape": "string" }, - { "name": "createOn", "displayName": "Created On", "shape": "date" }, - { - "name": "companyId", - "displayName": "Company ID", - "computed": true, - "valueExpression": "{{object.rawId}}", - "shape": "guid", - "visible": false - } + { "name": "createOn", "displayName": "Created On", "shape": "date" } ], "timeframes": false } diff --git a/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json b/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json index aadf817a..5931a4c0 100644 --- a/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json +++ b/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json @@ -1,7 +1,7 @@ { "name": "companyAccountBackupStatus", "displayName": "Company Account Backup Status", - "description": "Recent backup operation status history per account for a company", + "description": "Latest backup run per account for a company (the full seven day history is available as an option)", "tags": ["Backup", "Account"], "baseDataSourceName": "httpRequestScopedSingle", "config": { @@ -17,8 +17,25 @@ "postRequestScript": "companyAccountBackupStatus.js" }, "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, + "ui": [ + { + "type": "checkbox", + "name": "includeHistory", + "label": "Include full history", + "defaultValue": false, + "help": "Returns every run in Redstor's rolling seven day window. Large datasets may exceed the amount of data a tile can return." + } + ], "metadata": [ { "name": "accountName", "displayName": "Account", "shape": "string", "role": "label" }, + { "name": "serviceId", "displayName": "Service ID", "shape": "number", "visible": false }, + { + "name": "serviceName", + "displayName": "Service", + "sourceId": "serviceId", + "sourceType": "Service", + "objectPropertyPath": "name" + }, { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, { "name": "overallStatus", @@ -36,7 +53,6 @@ }, { "name": "message", "displayName": "Message", "shape": "string" }, { "name": "errorCount", "displayName": "Error Count", "shape": "number" }, - { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, { "name": "accountId", "displayName": "Account ID", "shape": "guid", "visible": false } ], "timeframes": false diff --git a/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json b/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json index e58472f5..0feaf50c 100644 --- a/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json +++ b/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json @@ -19,6 +19,13 @@ "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, "metadata": [ { "name": "accountName", "displayName": "Account", "shape": "string", "role": "label" }, + { + "name": "serviceName", + "displayName": "Service", + "sourceId": "productId", + "sourceType": "Service", + "objectPropertyPath": "name" + }, { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, { "name": "status", @@ -26,7 +33,13 @@ "shape": "number", "help": "Raw Redstor status code — RedAPI's public documentation does not define the meaning of each value." }, - { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, + { + "name": "productId", + "displayName": "Service ID", + "shape": "number", + "visible": false, + "help": "RedAPI names this field productId, but it carries service ids (94 = Exchange, 91 = OneDrive, 92 = SharePoint, 107 = Teams)" + }, { "name": "accountId", "displayName": "Account ID", "shape": "guid", "visible": false }, { "name": "companyId", "displayName": "Company ID", "shape": "guid", "visible": false }, { "name": "groupId", "displayName": "Group ID", "shape": "string", "visible": false } diff --git a/plugins/Redstor/v1/dataStreams/companySubscriptions.json b/plugins/Redstor/v1/dataStreams/companySubscriptions.json index 75902633..1ce4db42 100644 --- a/plugins/Redstor/v1/dataStreams/companySubscriptions.json +++ b/plugins/Redstor/v1/dataStreams/companySubscriptions.json @@ -14,13 +14,29 @@ "in": { "realm": "payload", "path": "nextPageToken" }, "out": { "realm": "queryArg", "path": "pageToken" } }, - "pathToData": "results" + "postRequestScript": "companySubscriptions.js" }, "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, "metadata": [ - { "name": "subscriptionId", "displayName": "Subscription ID", "shape": "guid", "role": "label" }, - { "name": "productId", "displayName": "Product ID", "shape": "number" }, - { "name": "editionId", "displayName": "Edition ID", "shape": "number" }, + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, + { "name": "editionId", "displayName": "Edition ID", "shape": "number", "visible": false }, + { "name": "productEditionKey", "displayName": "Product Edition Key", "shape": "string", "visible": false }, + { + "name": "productName", + "displayName": "Product", + "sourceId": "productId", + "sourceType": "Product", + "objectPropertyPath": "name", + "role": "label" + }, + { + "name": "editionName", + "displayName": "Edition", + "sourceId": "productEditionKey", + "sourceType": "Edition", + "objectPropertyPath": "name" + }, + { "name": "subscriptionId", "displayName": "Subscription ID", "shape": "guid" }, { "name": "isActive", "displayName": "Active", "shape": "boolean" }, { "name": "trialStatus", diff --git a/plugins/Redstor/v1/dataStreams/productEditions.json b/plugins/Redstor/v1/dataStreams/productEditions.json new file mode 100644 index 00000000..ce9f0414 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/productEditions.json @@ -0,0 +1,28 @@ +{ + "name": "productEditions", + "displayName": "Product Editions", + "description": "Editions available for each Redstor product", + "tags": ["Products"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/products", + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "postRequestScript": "productEditions.js" + }, + "matches": "none", + "metadata": [ + { "name": "productEditionKey", "displayName": "Product Edition Key", "shape": "string", "visible": false }, + { "name": "editionId", "displayName": "Edition ID", "shape": "number", "visible": false }, + { "name": "editionName", "displayName": "Edition", "shape": "string", "role": "label" }, + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, + { "name": "productName", "displayName": "Product", "shape": "string" }, + { "name": "hasTrial", "displayName": "Trial Available", "shape": "boolean" } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/productServices.json b/plugins/Redstor/v1/dataStreams/productServices.json new file mode 100644 index 00000000..010b649f --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/productServices.json @@ -0,0 +1,26 @@ +{ + "name": "productServices", + "displayName": "Product Services", + "description": "Services available within each Redstor product (Exchange and OneDrive sit within O365)", + "tags": ["Products"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/products", + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "postRequestScript": "productServices.js" + }, + "matches": "none", + "metadata": [ + { "name": "serviceId", "displayName": "Service ID", "shape": "number", "visible": false }, + { "name": "serviceName", "displayName": "Service", "shape": "string", "role": "label" }, + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, + { "name": "productName", "displayName": "Product", "shape": "string" } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/products.json b/plugins/Redstor/v1/dataStreams/products.json new file mode 100644 index 00000000..b583fd52 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/products.json @@ -0,0 +1,24 @@ +{ + "name": "products", + "displayName": "Products", + "description": "Redstor product catalog", + "tags": ["Products"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/products", + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "pathToData": "results" + }, + "matches": "none", + "metadata": [ + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, + { "name": "productName", "displayName": "Product", "shape": "string", "role": "label" } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/scripts/companyAccountBackupStatus.js b/plugins/Redstor/v1/dataStreams/scripts/companyAccountBackupStatus.js index f1843e0d..d43039eb 100644 --- a/plugins/Redstor/v1/dataStreams/scripts/companyAccountBackupStatus.js +++ b/plugins/Redstor/v1/dataStreams/scripts/companyAccountBackupStatus.js @@ -1,15 +1,23 @@ -// GET /backups/status/accounts returns one entry per account, each carrying -// a `statusHistory` array of recent runs — flatten to one row per run. +// GET /backups/status/accounts returns one entry per account, each carrying a +// `statusHistory` array covering a fixed rolling seven day window. Flattening every +// run produces roughly seven rows per account, which on a large company approaches +// the response size limit, so only the latest run per account is kept by default. +// The endpoint's `productId` field actually carries service ids (94 = Exchange, +// 91 = OneDrive, 92 = SharePoint, 107 = Teams), not the product ids used elsewhere. const accounts = data?.results || []; +const includeHistory = context.config.includeHistory === true; -result = accounts.flatMap((account) => - (account.statusHistory || []).map((entry) => ({ +result = accounts.flatMap((account) => { + const history = account.statusHistory || []; + const entries = includeHistory ? history : _.take(_.orderBy(history, ["timestamp"], ["desc"]), 1); + + return entries.map((entry) => ({ accountId: account.accountId, accountName: account.accountName, - productId: account.productId, + serviceId: account.productId, timestamp: entry.timestamp, status: entry.status, message: entry.message, errorCount: entry.errorCount, - })), -); + })); +}); diff --git a/plugins/Redstor/v1/dataStreams/scripts/companySubscriptions.js b/plugins/Redstor/v1/dataStreams/scripts/companySubscriptions.js new file mode 100644 index 00000000..6863535c --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/companySubscriptions.js @@ -0,0 +1,10 @@ +// Edition ids are only unique within a product, so Edition objects are keyed on a +// composite product-edition key and rows need that same key to resolve against them. +// It has to be built here rather than as a computed metadata column, because an object +// lookup resolves its sourceId against the response body and ignores computed values. +const subscriptions = data?.results || []; + +result = subscriptions.map((subscription) => ({ + ...subscription, + productEditionKey: subscription.productId + "-" + subscription.editionId, +})); diff --git a/plugins/Redstor/v1/dataStreams/scripts/errorHandling/accountBackupErrors.js b/plugins/Redstor/v1/dataStreams/scripts/errorHandling/accountBackupErrors.js new file mode 100644 index 00000000..ae48f8aa --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/errorHandling/accountBackupErrors.js @@ -0,0 +1,7 @@ +// The errors endpoint only recognises accounts that have a backup status record. +// Around a third of storage accounts have never run a backup and return a 422 +// rather than an empty result, so explain that rather than surfacing the raw error. +result = + response.status === 422 + ? "This account has no backup history in Redstor, so there are no error messages to report" + : (data && (data.detail || data.title)) || "Request failed with HTTP " + response.status; diff --git a/plugins/Redstor/v1/dataStreams/scripts/errorHandling/accountRestoreErrors.js b/plugins/Redstor/v1/dataStreams/scripts/errorHandling/accountRestoreErrors.js new file mode 100644 index 00000000..60e40d27 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/errorHandling/accountRestoreErrors.js @@ -0,0 +1,6 @@ +// The errors endpoint only recognises accounts that have a restore status record, +// and returns a 422 for any other account rather than an empty result. +result = + response.status === 422 + ? "This account has no restore history in Redstor, so there are no error messages to report" + : (data && (data.detail || data.title)) || "Request failed with HTTP " + response.status; diff --git a/plugins/Redstor/v1/dataStreams/scripts/productEditions.js b/plugins/Redstor/v1/dataStreams/scripts/productEditions.js new file mode 100644 index 00000000..5c41c98b --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/productEditions.js @@ -0,0 +1,15 @@ +// GET /products nests editions inside each product — flatten to one row per edition. +// Edition ids are only unique within a product (every product currently returns +// editionId 5, "Premium"), so rows carry a composite product-edition key for indexing. +const products = data?.results || []; + +result = products.flatMap((product) => + (product.editions || []).map((edition) => ({ + productEditionKey: product.productId + "-" + edition.editionId, + editionId: edition.editionId, + editionName: edition.editionName, + productId: product.productId, + productName: product.productName, + hasTrial: edition.hasTrial, + })), +); diff --git a/plugins/Redstor/v1/dataStreams/scripts/productServices.js b/plugins/Redstor/v1/dataStreams/scripts/productServices.js new file mode 100644 index 00000000..79a19d2e --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/productServices.js @@ -0,0 +1,11 @@ +// GET /products nests services inside each product — flatten to one row per service. +const products = data?.results || []; + +result = products.flatMap((product) => + (product.services || []).map((service) => ({ + serviceId: service.serviceId, + serviceName: service.serviceName, + productId: product.productId, + productName: product.productName, + })), +); diff --git a/plugins/Redstor/v1/defaultContent/overview.dash.json b/plugins/Redstor/v1/defaultContent/overview.dash.json index 76c36c09..d3997f0e 100644 --- a/plugins/Redstor/v1/defaultContent/overview.dash.json +++ b/plugins/Redstor/v1/defaultContent/overview.dash.json @@ -19,14 +19,14 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Accounts", + "title": "Backup Accounts", "description": "", "activePluginConfigIds": ["{{configId}}"], "timeframe": "none", "variables": ["{{variables.[Company]}}"], "dataStream": { - "id": "{{dataStreams.accounts}}", - "name": "accounts", + "id": "{{dataStreams.backupAccounts}}", + "name": "backupAccounts", "pluginConfigId": "{{configId}}", "group": { "by": [], "aggregate": [{ "type": "count" }] } }, @@ -333,7 +333,7 @@ "config": { "data-stream-table": { "transpose": false, - "columnOrder": ["productId", "editionId", "trialStatus", "isActive", "subscriptionId"] + "columnOrder": ["productName", "editionName", "trialStatus", "isActive", "subscriptionId"] } } } @@ -350,14 +350,14 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Accounts", + "title": "Backup Accounts", "description": "", "activePluginConfigIds": ["{{configId}}"], "timeframe": "none", "variables": ["{{variables.[Company]}}"], "dataStream": { - "id": "{{dataStreams.accounts}}", - "name": "accounts", + "id": "{{dataStreams.backupAccounts}}", + "name": "backupAccounts", "pluginConfigId": "{{configId}}" }, "scope": { diff --git a/plugins/Redstor/v1/docs/README.md b/plugins/Redstor/v1/docs/README.md index 68993bf6..b252f43f 100644 --- a/plugins/Redstor/v1/docs/README.md +++ b/plugins/Redstor/v1/docs/README.md @@ -1,4 +1,4 @@ -Monitor your [Redstor](https://www.redstor.com) Partner account in SquaredUp — customer companies, backup accounts, backup and restore status, storage consumption, and product subscriptions — via the [RedAPI](https://www.redstor.com/our-technology/integrations/redapi/). +Monitor your [Redstor](https://www.redstor.com) Partner account in SquaredUp — customer companies, seat and storage consumption, backup and restore health, and product subscriptions — via the [RedAPI](https://www.redstor.com/our-technology/integrations/redapi/). > ⚠️ RedAPI is only available to **Partner Admin** users on current Redstor pricing plans — Company Admins and legacy-plan accounts cannot use it. This plugin does not cover Redstor's older Storage Platform REST API. @@ -19,47 +19,60 @@ You will need a RedAPI **service account** with its Client ID and private key, p | ----- | ---------- | ---------------- | -------- | | **Company ID** | Your Redstor Partner company's ID. Scopes every API call to this company and its direct customers. | RedApp → **Company Settings**. | Yes | | **Client ID** | Identifies the RedAPI service account used to authenticate. | RedApp → **RedAPI → Service accounts** → your service account's key. | Yes | -| **Private Key** | The JSON Web Key (JWK) paired with the Client ID; signs the client assertion used to obtain access tokens. | Downloaded when the service account's key was created. | Yes | +| **Private Key** | The JSON Web Key (JWK) paired with the Client ID; signs the request used to obtain access tokens. | Downloaded when the service account's key was created. | Yes | -On save, the plugin authenticates by exchanging a signed JWT for a Redstor access token and calling your company's profile; an invalid Company ID, Client ID, or Private Key fails setup with an authentication error. +On save, the plugin authenticates and calls your company's profile; an invalid Company ID, Client ID, or Private Key fails setup with an authentication error. ## What this plugin monitors -- **Companies** — your Partner account and its customer companies. -- **Accounts** — backup and storage accounts, one per product/service a company runs. -- **Backup & restore status** — succeeded/failed/missed counts by product, plus recent per-account run history. -- **Consumption & subscriptions** — storage and seat usage, and active product subscriptions, per company. -- **Account errors** — backup and restore error and warning messages for a specific account. +- **Customer companies** — your Partner company and the customers beneath it. +- **Seat usage and billing exposure** — active, inactive and shared seats, licensed users, data protected and fair use overage, broken down by product for each company. +- **Backup and restore health** — succeeded, warning, failed and missed counts by product, plus the latest run for every individual backup account. +- **Backup accounts** — every protected mailbox, drive, site and machine, with the product and service it belongs to. +- **Subscriptions** — which products each company is subscribed to, and whether each is on trial. + +**Seats and accounts are not the same number.** Redstor bills per seat, where a seat is one person. A single seat usually holds several backup accounts: a Microsoft 365 seat is an Exchange account plus a OneDrive account, and SharePoint and Teams sites add more on top. A company's account count will therefore be considerably higher than its seat count, so take seat figures from **Company Consumption** rather than by counting accounts. The out-of-the-box dashboards include a Company **Overview** and a perspective for each **Account**. ## Data streams -- **Company Accounts** — backup/storage accounts for a company, one row per account. -- **Company Backup Status** — current backup status by product for a company. -- **Company Restore Status** — current restore status by product for a company. -- **Company Consumption** — storage and seat consumption by product for a company. -- **Company Subscriptions** — product subscriptions for a company. -- **Company Account Backup Status** — recent backup run history per account for a company. -- **Company Account Restore Status** — current restore status per account for a company. -- **Account Backup Errors** — backup error and warning messages for an account. -- **Account Restore Errors** — restore error and warning messages for an account. +- **Company Consumption** — seat counts, licensed users, data protected and fair use overage by product, for a company. +- **Company Subscriptions** — product subscriptions for a company, with the product and edition named and the trial status shown. +- **Company Backup Status** — backup results by product for a company, counting succeeded, warnings, errors, failures and missed runs. +- **Company Restore Status** — restore results by product for a company. +- **Company Account Backup Status** — the latest backup run for every account in a company, with an option to return the full history Redstor holds. +- **Company Account Restore Status** — the latest restore state for every account in a company. +- **Backup Accounts** — every backup account in a company, one row per account, with its product, service, storage region and creation date. +- **Account Backup Errors** — backup error and warning messages for a single account. +- **Account Restore Errors** — restore error and warning messages for a single account. +- **Products** — the Redstor product catalog. +- **Product Services** — the services within each product, such as Exchange and OneDrive within O365. +- **Product Editions** — the editions available for each product, and which of them offer a trial. ## What gets indexed | Object type | API source | Represents | | ----------- | ---------- | ---------- | | **Company** | `GET /companies/{companyId}`, `GET /companies/{companyId}/customers` | Your configured Partner company and its direct customer companies. | -| **Account** | `GET /storage/accounts` | A backup or storage account belonging to a company, for one product/service. | +| **Account** | `GET /storage/accounts` | A single backup account: one mailbox, drive, site or machine. Not the same as a seat, as described above. | +| **Product** | `GET /products` | A product a company can subscribe to and be billed for, such as O365 or Machines. | +| **Service** | `GET /products` | A workload within a product, such as Exchange, OneDrive, SharePoint or Teams within O365. | +| **Edition** | `GET /products` | An edition of a product, such as Premium. | **Relationships:** each Account belongs to its parent Company. +Seats are not indexed as objects, because Redstor reports them as counts rather than as individually addressable records. They appear in **Company Consumption**. + ## Known limitations -- **Only partially verified against a live Redstor tenant** — authentication (the JWK-signed `private_key_jwt` exchange) and the Company Profile endpoint have been confirmed working end-to-end against a real Partner account. Every other data stream was still only checked field-by-field against RedAPI's public OpenAPI specification (`https://assets.redstor.com/api-gateway/swagger-ui/public.json`), which is not fully reliable — the spec's documented auth header (`X-Api-Key`) turned out to be wrong; the live API actually requires a standard `Authorization: Bearer` header plus an `apiVersion=2.0` query parameter the spec lists as optional. Verify the remaining data streams against a live tenant before relying on them in production. -- **No historical or time-range data** — RedAPI exposes no `from`/`to` range parameter on any endpoint; every stream returns a current-state snapshot, and dashboards have no timeframe picker. -- **Restore status codes are undocumented** — the numeric `status` values in Company Account Restore Status aren't defined anywhere in RedAPI's public documentation, so they're shown as a raw number rather than mapped to a health color. (Company Account Backup Status's codes *are* documented and are mapped to a status color.) -- **Subscriptions show IDs, not names** — RedAPI's `/subscriptions` endpoint returns `productId`/`editionId` only, with no product name lookup; this plugin doesn't index a Products type, so subscription rows show numeric IDs. -- **Company hierarchy is one level deep** — only the configured Partner company and its direct customers are indexed; deeper reseller-of-reseller chains aren't walked recursively. -- **Subject to Redstor's fair-use throttling** — RedAPI enforces rate limiting; specific limits aren't published. +- **No historical data** — every stream reports the current state, as RedAPI exposes no time ranges, so tiles have no timeframe selection. Consumption is the one partial exception: it can report a single past date, but not a range or a trend. +- **Backup history is limited and can be large** — Company Account Backup Status shows the latest run per account by default. Enabling **Include full history** returns every run in Redstor's rolling seven day window, which on a large customer can be more data than a single tile can return. +- **Accounts that have never run a backup have no error detail** — Redstor only keeps error messages for accounts that have backup history. The Account Backup Errors and Account Restore Errors streams say so rather than returning rows. In testing this applied to roughly a third of a company's accounts. +- **Machine products report workloads, not seats** — Machines and Azure Virtual Machines return zero for every seat count and are measured by workload count instead, so seat-based tiles will look empty for those products. +- **No billable seat total** — Redstor reports active, inactive and shared seats separately but publishes no chargeable figure, so billing has to be worked out from your own agreement and Redstor's fair use rules rather than read directly. +- **Restore status codes are not documented** — Redstor publishes no meaning for the numeric restore status values, so they appear as a raw number rather than a health colour. Backup status codes are documented and do show as colours. On a company with no restore activity, the stream lists accounts with no status against them. +- **No groups or collections** — Redstor's grouping features are not available through the API and cannot be reported on. +- **Only direct customers are included** — your Partner company and the customers directly beneath it are indexed; deeper reseller-of-reseller chains are not followed. +- **Subject to Redstor's fair use throttling** — RedAPI is rate limited and the limits are not published, so very large estates may import slowly. - **Read-only** — the plugin never creates, modifies, or deletes anything in Redstor. diff --git a/plugins/Redstor/v1/indexDefinitions/default.json b/plugins/Redstor/v1/indexDefinitions/default.json index 73f9e362..9c8343b9 100644 --- a/plugins/Redstor/v1/indexDefinitions/default.json +++ b/plugins/Redstor/v1/indexDefinitions/default.json @@ -23,8 +23,40 @@ } }, { - "name": "accounts", - "dataStream": { "name": "accounts" }, + "name": "products", + "dataStream": { "name": "products" }, + "timeframe": "none", + "objectMapping": { + "id": "productId", + "name": "productName", + "type": { "value": "Product" } + } + }, + { + "name": "productServices", + "dataStream": { "name": "productServices" }, + "timeframe": "none", + "objectMapping": { + "id": "serviceId", + "name": "serviceName", + "type": { "value": "Service" }, + "properties": ["productId", "productName"] + } + }, + { + "name": "productEditions", + "dataStream": { "name": "productEditions" }, + "timeframe": "none", + "objectMapping": { + "id": "productEditionKey", + "name": "editionName", + "type": { "value": "Edition" }, + "properties": ["editionId", "productId", "productName", "hasTrial"] + } + }, + { + "name": "backupAccounts", + "dataStream": { "name": "backupAccounts" }, "scope": { "query": "g.V().has(\"sourceType\", \"Company\")" }, @@ -40,8 +72,7 @@ "productId", "productName", "storageRegion", - "createOn", - "companyId" + "createOn" ] }, "dependsOn": ["companyProfile", "companyCustomers"] diff --git a/plugins/Redstor/v1/metadata.json b/plugins/Redstor/v1/metadata.json index f1b840b5..78a64457 100644 --- a/plugins/Redstor/v1/metadata.json +++ b/plugins/Redstor/v1/metadata.json @@ -10,7 +10,7 @@ "importNotSupported": false, "restrictedToPlatforms": [], "keywords": ["backup", "disaster recovery", "archiving", "msp", "redstor", "redapi"], - "objectTypes": ["Company", "Account"], + "objectTypes": ["Company", "Account", "Product", "Service", "Edition"], "links": [ { "category": "documentation", diff --git a/plugins/Redstor/v1/ui.json b/plugins/Redstor/v1/ui.json index f9fabc82..da63360b 100644 --- a/plugins/Redstor/v1/ui.json +++ b/plugins/Redstor/v1/ui.json @@ -8,7 +8,7 @@ "validation": { "required": true } }, { - "type": "text", + "type": "password", "name": "clientId", "label": "Client ID", "allowEncryption": true, From 1788c8f1b5af5406afb384dc05e4ff5528f54f70 Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Thu, 10 Sep 2026 08:03:55 +0100 Subject: [PATCH 05/24] Fix Redstor Edition indexing to use edition ID, not a composite key Edition IDs are globally unique and not scoped per product, so the composite productId-editionId key was unnecessary. Index Edition objects on editionId directly and drop the now-unneeded productEditionKey construction from productEditions and companySubscriptions. Co-Authored-By: Claude Sonnet 5 --- .../Redstor/v1/dataStreams/companySubscriptions.json | 5 ++--- plugins/Redstor/v1/dataStreams/productEditions.json | 1 - .../v1/dataStreams/scripts/companySubscriptions.js | 10 ---------- .../Redstor/v1/dataStreams/scripts/productEditions.js | 3 --- plugins/Redstor/v1/indexDefinitions/default.json | 4 ++-- 5 files changed, 4 insertions(+), 19 deletions(-) delete mode 100644 plugins/Redstor/v1/dataStreams/scripts/companySubscriptions.js diff --git a/plugins/Redstor/v1/dataStreams/companySubscriptions.json b/plugins/Redstor/v1/dataStreams/companySubscriptions.json index 1ce4db42..42b9f9ea 100644 --- a/plugins/Redstor/v1/dataStreams/companySubscriptions.json +++ b/plugins/Redstor/v1/dataStreams/companySubscriptions.json @@ -14,13 +14,12 @@ "in": { "realm": "payload", "path": "nextPageToken" }, "out": { "realm": "queryArg", "path": "pageToken" } }, - "postRequestScript": "companySubscriptions.js" + "pathToData": "results" }, "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, "metadata": [ { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, { "name": "editionId", "displayName": "Edition ID", "shape": "number", "visible": false }, - { "name": "productEditionKey", "displayName": "Product Edition Key", "shape": "string", "visible": false }, { "name": "productName", "displayName": "Product", @@ -32,7 +31,7 @@ { "name": "editionName", "displayName": "Edition", - "sourceId": "productEditionKey", + "sourceId": "editionId", "sourceType": "Edition", "objectPropertyPath": "name" }, diff --git a/plugins/Redstor/v1/dataStreams/productEditions.json b/plugins/Redstor/v1/dataStreams/productEditions.json index ce9f0414..67bcca8f 100644 --- a/plugins/Redstor/v1/dataStreams/productEditions.json +++ b/plugins/Redstor/v1/dataStreams/productEditions.json @@ -17,7 +17,6 @@ }, "matches": "none", "metadata": [ - { "name": "productEditionKey", "displayName": "Product Edition Key", "shape": "string", "visible": false }, { "name": "editionId", "displayName": "Edition ID", "shape": "number", "visible": false }, { "name": "editionName", "displayName": "Edition", "shape": "string", "role": "label" }, { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, diff --git a/plugins/Redstor/v1/dataStreams/scripts/companySubscriptions.js b/plugins/Redstor/v1/dataStreams/scripts/companySubscriptions.js deleted file mode 100644 index 6863535c..00000000 --- a/plugins/Redstor/v1/dataStreams/scripts/companySubscriptions.js +++ /dev/null @@ -1,10 +0,0 @@ -// Edition ids are only unique within a product, so Edition objects are keyed on a -// composite product-edition key and rows need that same key to resolve against them. -// It has to be built here rather than as a computed metadata column, because an object -// lookup resolves its sourceId against the response body and ignores computed values. -const subscriptions = data?.results || []; - -result = subscriptions.map((subscription) => ({ - ...subscription, - productEditionKey: subscription.productId + "-" + subscription.editionId, -})); diff --git a/plugins/Redstor/v1/dataStreams/scripts/productEditions.js b/plugins/Redstor/v1/dataStreams/scripts/productEditions.js index 5c41c98b..f66d9c75 100644 --- a/plugins/Redstor/v1/dataStreams/scripts/productEditions.js +++ b/plugins/Redstor/v1/dataStreams/scripts/productEditions.js @@ -1,11 +1,8 @@ // GET /products nests editions inside each product — flatten to one row per edition. -// Edition ids are only unique within a product (every product currently returns -// editionId 5, "Premium"), so rows carry a composite product-edition key for indexing. const products = data?.results || []; result = products.flatMap((product) => (product.editions || []).map((edition) => ({ - productEditionKey: product.productId + "-" + edition.editionId, editionId: edition.editionId, editionName: edition.editionName, productId: product.productId, diff --git a/plugins/Redstor/v1/indexDefinitions/default.json b/plugins/Redstor/v1/indexDefinitions/default.json index 9c8343b9..f852a23e 100644 --- a/plugins/Redstor/v1/indexDefinitions/default.json +++ b/plugins/Redstor/v1/indexDefinitions/default.json @@ -48,10 +48,10 @@ "dataStream": { "name": "productEditions" }, "timeframe": "none", "objectMapping": { - "id": "productEditionKey", + "id": "editionId", "name": "editionName", "type": { "value": "Edition" }, - "properties": ["editionId", "productId", "productName", "hasTrial"] + "properties": ["hasTrial"] } }, { From 04eaba5964cb92a3407abbf011a6afe1600cc364 Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Fri, 11 Sep 2026 16:40:04 +0100 Subject: [PATCH 06/24] Restore Account-to-Company link and tidy up Redstor data streams - Restore companyId on Account objects via a postRequestScript reading context.objects[0].rawId. The earlier attempt used {{object.rawId}} in a metadata valueExpression, which only resolves in request-building context (getArgs/endpointPath), not response shaping, so it failed on every row and was removed. This puts the property back the correct way. - Hide Products, Product Services, Product Editions and Company Subscriptions from the tile picker - they're reference/enrichment streams, not ones users pick directly. - Normalise data stream tags down to Account, Company and Usage. - Rename Backup Accounts to Company Accounts for clarity, updating the matching OOB tile titles and README entry. Co-Authored-By: Claude Sonnet 5 --- plugins/Redstor/v1/dataStreams/accountBackupErrors.json | 2 +- plugins/Redstor/v1/dataStreams/accountRestoreErrors.json | 2 +- plugins/Redstor/v1/dataStreams/backupAccounts.json | 7 ++++--- .../v1/dataStreams/companyAccountBackupStatus.json | 2 +- .../v1/dataStreams/companyAccountRestoreStatus.json | 2 +- plugins/Redstor/v1/dataStreams/companyBackupStatus.json | 2 +- plugins/Redstor/v1/dataStreams/companyRestoreStatus.json | 2 +- plugins/Redstor/v1/dataStreams/companySubscriptions.json | 3 ++- plugins/Redstor/v1/dataStreams/productEditions.json | 3 ++- plugins/Redstor/v1/dataStreams/productServices.json | 3 ++- plugins/Redstor/v1/dataStreams/products.json | 3 ++- plugins/Redstor/v1/dataStreams/scripts/backupAccounts.js | 9 +++++++++ plugins/Redstor/v1/defaultContent/overview.dash.json | 4 ++-- plugins/Redstor/v1/docs/README.md | 2 +- plugins/Redstor/v1/indexDefinitions/default.json | 3 ++- 15 files changed, 32 insertions(+), 17 deletions(-) create mode 100644 plugins/Redstor/v1/dataStreams/scripts/backupAccounts.js diff --git a/plugins/Redstor/v1/dataStreams/accountBackupErrors.json b/plugins/Redstor/v1/dataStreams/accountBackupErrors.json index 23d09998..d313e7b6 100644 --- a/plugins/Redstor/v1/dataStreams/accountBackupErrors.json +++ b/plugins/Redstor/v1/dataStreams/accountBackupErrors.json @@ -2,7 +2,7 @@ "name": "accountBackupErrors", "displayName": "Account Backup Errors", "description": "Backup error and warning messages for an account", - "tags": ["Backup", "Account"], + "tags": ["Account"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json b/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json index 940b9377..3aef08cd 100644 --- a/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json +++ b/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json @@ -2,7 +2,7 @@ "name": "accountRestoreErrors", "displayName": "Account Restore Errors", "description": "Restore error and warning messages for an account", - "tags": ["Backup", "Account"], + "tags": ["Account"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/backupAccounts.json b/plugins/Redstor/v1/dataStreams/backupAccounts.json index b0757546..6e21164e 100644 --- a/plugins/Redstor/v1/dataStreams/backupAccounts.json +++ b/plugins/Redstor/v1/dataStreams/backupAccounts.json @@ -1,6 +1,6 @@ { "name": "backupAccounts", - "displayName": "Backup Accounts", + "displayName": "Company Accounts", "description": "Backup accounts for a company (one row per account, and a single seat can hold more than one)", "tags": ["Account"], "baseDataSourceName": "httpRequestScopedSingle", @@ -14,7 +14,7 @@ "in": { "realm": "payload", "path": "nextPageToken" }, "out": { "realm": "queryArg", "path": "pageToken" } }, - "pathToData": "results" + "postRequestScript": "backupAccounts.js" }, "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, "metadata": [ @@ -26,7 +26,8 @@ { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, { "name": "productName", "displayName": "Product", "shape": "string" }, { "name": "storageRegion", "displayName": "Storage Region", "shape": "string" }, - { "name": "createOn", "displayName": "Created On", "shape": "date" } + { "name": "createOn", "displayName": "Created On", "shape": "date" }, + { "name": "companyId", "displayName": "Company ID", "shape": "guid", "visible": false } ], "timeframes": false } diff --git a/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json b/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json index 5931a4c0..6d0b89f3 100644 --- a/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json +++ b/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json @@ -2,7 +2,7 @@ "name": "companyAccountBackupStatus", "displayName": "Company Account Backup Status", "description": "Latest backup run per account for a company (the full seven day history is available as an option)", - "tags": ["Backup", "Account"], + "tags": ["Account"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json b/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json index 0feaf50c..be7bd295 100644 --- a/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json +++ b/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json @@ -2,7 +2,7 @@ "name": "companyAccountRestoreStatus", "displayName": "Company Account Restore Status", "description": "Current restore operation status per account for a company", - "tags": ["Backup", "Account"], + "tags": ["Account"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/companyBackupStatus.json b/plugins/Redstor/v1/dataStreams/companyBackupStatus.json index 6023689d..9c91c657 100644 --- a/plugins/Redstor/v1/dataStreams/companyBackupStatus.json +++ b/plugins/Redstor/v1/dataStreams/companyBackupStatus.json @@ -2,7 +2,7 @@ "name": "companyBackupStatus", "displayName": "Company Backup Status", "description": "Current backup operation status by product for a company", - "tags": ["Backup", "Company"], + "tags": ["Company"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/companyRestoreStatus.json b/plugins/Redstor/v1/dataStreams/companyRestoreStatus.json index a9dedf5d..4df53024 100644 --- a/plugins/Redstor/v1/dataStreams/companyRestoreStatus.json +++ b/plugins/Redstor/v1/dataStreams/companyRestoreStatus.json @@ -2,7 +2,7 @@ "name": "companyRestoreStatus", "displayName": "Company Restore Status", "description": "Current restore operation status by product for a company", - "tags": ["Backup", "Company"], + "tags": ["Company"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/companySubscriptions.json b/plugins/Redstor/v1/dataStreams/companySubscriptions.json index 42b9f9ea..e545b0ee 100644 --- a/plugins/Redstor/v1/dataStreams/companySubscriptions.json +++ b/plugins/Redstor/v1/dataStreams/companySubscriptions.json @@ -2,7 +2,7 @@ "name": "companySubscriptions", "displayName": "Company Subscriptions", "description": "Product subscriptions for a company", - "tags": ["Subscriptions", "Company"], + "tags": ["Company"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", @@ -17,6 +17,7 @@ "pathToData": "results" }, "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, + "visibility": { "type": "hidden" }, "metadata": [ { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, { "name": "editionId", "displayName": "Edition ID", "shape": "number", "visible": false }, diff --git a/plugins/Redstor/v1/dataStreams/productEditions.json b/plugins/Redstor/v1/dataStreams/productEditions.json index 67bcca8f..0f6d82ec 100644 --- a/plugins/Redstor/v1/dataStreams/productEditions.json +++ b/plugins/Redstor/v1/dataStreams/productEditions.json @@ -2,7 +2,7 @@ "name": "productEditions", "displayName": "Product Editions", "description": "Editions available for each Redstor product", - "tags": ["Products"], + "tags": ["Usage"], "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", @@ -16,6 +16,7 @@ "postRequestScript": "productEditions.js" }, "matches": "none", + "visibility": { "type": "hidden" }, "metadata": [ { "name": "editionId", "displayName": "Edition ID", "shape": "number", "visible": false }, { "name": "editionName", "displayName": "Edition", "shape": "string", "role": "label" }, diff --git a/plugins/Redstor/v1/dataStreams/productServices.json b/plugins/Redstor/v1/dataStreams/productServices.json index 010b649f..cc1b7fcd 100644 --- a/plugins/Redstor/v1/dataStreams/productServices.json +++ b/plugins/Redstor/v1/dataStreams/productServices.json @@ -2,7 +2,7 @@ "name": "productServices", "displayName": "Product Services", "description": "Services available within each Redstor product (Exchange and OneDrive sit within O365)", - "tags": ["Products"], + "tags": ["Usage"], "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", @@ -16,6 +16,7 @@ "postRequestScript": "productServices.js" }, "matches": "none", + "visibility": { "type": "hidden" }, "metadata": [ { "name": "serviceId", "displayName": "Service ID", "shape": "number", "visible": false }, { "name": "serviceName", "displayName": "Service", "shape": "string", "role": "label" }, diff --git a/plugins/Redstor/v1/dataStreams/products.json b/plugins/Redstor/v1/dataStreams/products.json index b583fd52..da14bef5 100644 --- a/plugins/Redstor/v1/dataStreams/products.json +++ b/plugins/Redstor/v1/dataStreams/products.json @@ -2,7 +2,7 @@ "name": "products", "displayName": "Products", "description": "Redstor product catalog", - "tags": ["Products"], + "tags": ["Usage"], "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", @@ -16,6 +16,7 @@ "pathToData": "results" }, "matches": "none", + "visibility": { "type": "hidden" }, "metadata": [ { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, { "name": "productName", "displayName": "Product", "shape": "string", "role": "label" } diff --git a/plugins/Redstor/v1/dataStreams/scripts/backupAccounts.js b/plugins/Redstor/v1/dataStreams/scripts/backupAccounts.js new file mode 100644 index 00000000..2bc84230 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/backupAccounts.js @@ -0,0 +1,9 @@ +// /storage/accounts doesn't echo the company id back onto each row, so stamp it +// from the scoped request object to relate accounts back to their company. +const accounts = data?.results || []; +const companyId = context.objects[0].rawId; + +result = accounts.map((account) => ({ + ...account, + companyId, +})); diff --git a/plugins/Redstor/v1/defaultContent/overview.dash.json b/plugins/Redstor/v1/defaultContent/overview.dash.json index d3997f0e..264a4e05 100644 --- a/plugins/Redstor/v1/defaultContent/overview.dash.json +++ b/plugins/Redstor/v1/defaultContent/overview.dash.json @@ -19,7 +19,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Backup Accounts", + "title": "Company Accounts", "description": "", "activePluginConfigIds": ["{{configId}}"], "timeframe": "none", @@ -350,7 +350,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Backup Accounts", + "title": "Company Accounts", "description": "", "activePluginConfigIds": ["{{configId}}"], "timeframe": "none", diff --git a/plugins/Redstor/v1/docs/README.md b/plugins/Redstor/v1/docs/README.md index b252f43f..9da14996 100644 --- a/plugins/Redstor/v1/docs/README.md +++ b/plugins/Redstor/v1/docs/README.md @@ -43,7 +43,7 @@ The out-of-the-box dashboards include a Company **Overview** and a perspective f - **Company Restore Status** — restore results by product for a company. - **Company Account Backup Status** — the latest backup run for every account in a company, with an option to return the full history Redstor holds. - **Company Account Restore Status** — the latest restore state for every account in a company. -- **Backup Accounts** — every backup account in a company, one row per account, with its product, service, storage region and creation date. +- **Company Accounts** — every backup account in a company, one row per account, with its product, service, storage region and creation date. - **Account Backup Errors** — backup error and warning messages for a single account. - **Account Restore Errors** — restore error and warning messages for a single account. - **Products** — the Redstor product catalog. diff --git a/plugins/Redstor/v1/indexDefinitions/default.json b/plugins/Redstor/v1/indexDefinitions/default.json index f852a23e..46736c15 100644 --- a/plugins/Redstor/v1/indexDefinitions/default.json +++ b/plugins/Redstor/v1/indexDefinitions/default.json @@ -72,7 +72,8 @@ "productId", "productName", "storageRegion", - "createOn" + "createOn", + "companyId" ] }, "dependsOn": ["companyProfile", "companyCustomers"] From fa973ea44d4d9e30063f58afe0fe2adda25bad35 Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Fri, 11 Sep 2026 21:57:36 +0100 Subject: [PATCH 07/24] Restructure Redstor backup/restore streams and fix RedAPI param usage - Rename Company Account Backup/Restore Status to Company Backups/Restores, and Company Backup/Restore Status to Company Backup/Restore Summary - Fix Company Restores to flatten statusHistory per account instead of reading timestamp/status fields that were never populated - Add Account Backups and Account Restores (single-account run history), scoped via companyId + accountId per RedAPI's public OpenAPI spec - Retag streams with functional Backup/Restore tags instead of Account/ Company; drop tags from Company Accounts and hidden reference streams - Drop Company Backup/Restore Errors - RedAPI's /errors endpoints require accountId with no company-wide equivalent, so this isn't achievable as a single data stream Co-Authored-By: Claude Sonnet 5 --- .../v1/dataStreams/accountBackupErrors.json | 2 +- .../v1/dataStreams/accountBackups.json | 43 +++++++++ .../v1/dataStreams/accountRestoreErrors.json | 2 +- .../v1/dataStreams/accountRestores.json | 33 +++++++ .../v1/dataStreams/backupAccounts.json | 1 - ...pStatus.json => companyBackupSummary.json} | 6 +- ...tBackupStatus.json => companyBackups.json} | 8 +- .../v1/dataStreams/companyConsumption.json | 2 +- .../v1/dataStreams/companyCustomers.json | 1 - .../v1/dataStreams/companyProfile.json | 1 - ...Status.json => companyRestoreSummary.json} | 6 +- ...estoreStatus.json => companyRestores.json} | 19 +++- .../v1/dataStreams/companySubscriptions.json | 1 - .../v1/dataStreams/productEditions.json | 1 - .../v1/dataStreams/productServices.json | 1 - plugins/Redstor/v1/dataStreams/products.json | 1 - .../v1/dataStreams/scripts/accountBackups.js | 12 +++ .../v1/dataStreams/scripts/accountRestores.js | 16 ++++ ...countBackupStatus.js => companyBackups.js} | 0 .../v1/dataStreams/scripts/companyRestores.js | 39 ++++++++ .../accountPerspective.dash.json | 76 +++++++++++++++ .../v1/defaultContent/overview.dash.json | 92 +++++++++++++++++-- plugins/Redstor/v1/docs/README.md | 14 ++- 23 files changed, 339 insertions(+), 38 deletions(-) create mode 100644 plugins/Redstor/v1/dataStreams/accountBackups.json create mode 100644 plugins/Redstor/v1/dataStreams/accountRestores.json rename plugins/Redstor/v1/dataStreams/{companyBackupStatus.json => companyBackupSummary.json} (94%) rename plugins/Redstor/v1/dataStreams/{companyAccountBackupStatus.json => companyBackups.json} (93%) rename plugins/Redstor/v1/dataStreams/{companyRestoreStatus.json => companyRestoreSummary.json} (93%) rename plugins/Redstor/v1/dataStreams/{companyAccountRestoreStatus.json => companyRestores.json} (74%) create mode 100644 plugins/Redstor/v1/dataStreams/scripts/accountBackups.js create mode 100644 plugins/Redstor/v1/dataStreams/scripts/accountRestores.js rename plugins/Redstor/v1/dataStreams/scripts/{companyAccountBackupStatus.js => companyBackups.js} (100%) create mode 100644 plugins/Redstor/v1/dataStreams/scripts/companyRestores.js diff --git a/plugins/Redstor/v1/dataStreams/accountBackupErrors.json b/plugins/Redstor/v1/dataStreams/accountBackupErrors.json index d313e7b6..285589f5 100644 --- a/plugins/Redstor/v1/dataStreams/accountBackupErrors.json +++ b/plugins/Redstor/v1/dataStreams/accountBackupErrors.json @@ -2,7 +2,7 @@ "name": "accountBackupErrors", "displayName": "Account Backup Errors", "description": "Backup error and warning messages for an account", - "tags": ["Account"], + "tags": ["Backup"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/accountBackups.json b/plugins/Redstor/v1/dataStreams/accountBackups.json new file mode 100644 index 00000000..8d47f690 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/accountBackups.json @@ -0,0 +1,43 @@ +{ + "name": "accountBackups", + "displayName": "Account Backups", + "description": "Backup run history for an account, over Redstor's rolling seven day window", + "tags": ["Backup"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/backups/status/accounts", + "getArgs": [ + { "key": "companyId", "value": "{{object.companyId}}" }, + { "key": "accountId", "value": "{{object.rawId}}" } + ], + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "postRequestScript": "accountBackups.js" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Account"] } }, + "metadata": [ + { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, + { + "name": "overallStatus", + "displayName": "Status", + "computed": true, + "valueExpression": "{{ $['status'] === 1 ? 'success' : ($['status'] === 2 ? 'warning' : 'error') }}", + "shape": "state" + }, + { + "name": "status", + "displayName": "Status Code", + "shape": "number", + "visible": false, + "help": "Raw Redstor status code: 0 = Failed, 1 = Completed successfully, 2 = Completed with warnings, 3 = Completed with errors." + }, + { "name": "message", "displayName": "Message", "shape": "string", "role": "label" }, + { "name": "errorCount", "displayName": "Error Count", "shape": "number" } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json b/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json index 3aef08cd..ead9da90 100644 --- a/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json +++ b/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json @@ -2,7 +2,7 @@ "name": "accountRestoreErrors", "displayName": "Account Restore Errors", "description": "Restore error and warning messages for an account", - "tags": ["Account"], + "tags": ["Restore"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/accountRestores.json b/plugins/Redstor/v1/dataStreams/accountRestores.json new file mode 100644 index 00000000..8edcbba2 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/accountRestores.json @@ -0,0 +1,33 @@ +{ + "name": "accountRestores", + "displayName": "Account Restores", + "description": "Restore run history for an account, over Redstor's rolling seven day window (where Redstor holds one)", + "tags": ["Restore"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/restores/status/accounts", + "getArgs": [ + { "key": "companyId", "value": "{{object.companyId}}" }, + { "key": "accountId", "value": "{{object.rawId}}" } + ], + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "postRequestScript": "accountRestores.js" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Account"] } }, + "metadata": [ + { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, + { + "name": "status", + "displayName": "Status Code", + "shape": "number", + "help": "Raw Redstor status code — RedAPI's public documentation does not define the meaning of each value." + } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/backupAccounts.json b/plugins/Redstor/v1/dataStreams/backupAccounts.json index 6e21164e..80489f24 100644 --- a/plugins/Redstor/v1/dataStreams/backupAccounts.json +++ b/plugins/Redstor/v1/dataStreams/backupAccounts.json @@ -2,7 +2,6 @@ "name": "backupAccounts", "displayName": "Company Accounts", "description": "Backup accounts for a company (one row per account, and a single seat can hold more than one)", - "tags": ["Account"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/companyBackupStatus.json b/plugins/Redstor/v1/dataStreams/companyBackupSummary.json similarity index 94% rename from plugins/Redstor/v1/dataStreams/companyBackupStatus.json rename to plugins/Redstor/v1/dataStreams/companyBackupSummary.json index 9c91c657..cc244371 100644 --- a/plugins/Redstor/v1/dataStreams/companyBackupStatus.json +++ b/plugins/Redstor/v1/dataStreams/companyBackupSummary.json @@ -1,8 +1,8 @@ { - "name": "companyBackupStatus", - "displayName": "Company Backup Status", + "name": "companyBackupSummary", + "displayName": "Company Backup Summary", "description": "Current backup operation status by product for a company", - "tags": ["Company"], + "tags": ["Backup"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json b/plugins/Redstor/v1/dataStreams/companyBackups.json similarity index 93% rename from plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json rename to plugins/Redstor/v1/dataStreams/companyBackups.json index 6d0b89f3..c4a7d370 100644 --- a/plugins/Redstor/v1/dataStreams/companyAccountBackupStatus.json +++ b/plugins/Redstor/v1/dataStreams/companyBackups.json @@ -1,8 +1,8 @@ { - "name": "companyAccountBackupStatus", - "displayName": "Company Account Backup Status", + "name": "companyBackups", + "displayName": "Company Backups", "description": "Latest backup run per account for a company (the full seven day history is available as an option)", - "tags": ["Account"], + "tags": ["Backup"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", @@ -14,7 +14,7 @@ "in": { "realm": "payload", "path": "nextPageToken" }, "out": { "realm": "queryArg", "path": "pageToken" } }, - "postRequestScript": "companyAccountBackupStatus.js" + "postRequestScript": "companyBackups.js" }, "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, "ui": [ diff --git a/plugins/Redstor/v1/dataStreams/companyConsumption.json b/plugins/Redstor/v1/dataStreams/companyConsumption.json index a39a38d8..734d241a 100644 --- a/plugins/Redstor/v1/dataStreams/companyConsumption.json +++ b/plugins/Redstor/v1/dataStreams/companyConsumption.json @@ -2,7 +2,7 @@ "name": "companyConsumption", "displayName": "Company Consumption", "description": "Storage consumption and seat usage by product for a company", - "tags": ["Usage", "Company"], + "tags": ["Usage"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/companyCustomers.json b/plugins/Redstor/v1/dataStreams/companyCustomers.json index 65e48ea0..b2202e28 100644 --- a/plugins/Redstor/v1/dataStreams/companyCustomers.json +++ b/plugins/Redstor/v1/dataStreams/companyCustomers.json @@ -2,7 +2,6 @@ "name": "companyCustomers", "displayName": "Company Customers", "description": "Customer companies under the configured Redstor Partner company", - "tags": ["Company"], "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/companyProfile.json b/plugins/Redstor/v1/dataStreams/companyProfile.json index 3767639a..b25d4671 100644 --- a/plugins/Redstor/v1/dataStreams/companyProfile.json +++ b/plugins/Redstor/v1/dataStreams/companyProfile.json @@ -2,7 +2,6 @@ "name": "companyProfile", "displayName": "Company Profile", "description": "The configured Redstor Partner company's own profile", - "tags": ["Company"], "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/companyRestoreStatus.json b/plugins/Redstor/v1/dataStreams/companyRestoreSummary.json similarity index 93% rename from plugins/Redstor/v1/dataStreams/companyRestoreStatus.json rename to plugins/Redstor/v1/dataStreams/companyRestoreSummary.json index 4df53024..da8edcc2 100644 --- a/plugins/Redstor/v1/dataStreams/companyRestoreStatus.json +++ b/plugins/Redstor/v1/dataStreams/companyRestoreSummary.json @@ -1,8 +1,8 @@ { - "name": "companyRestoreStatus", - "displayName": "Company Restore Status", + "name": "companyRestoreSummary", + "displayName": "Company Restore Summary", "description": "Current restore operation status by product for a company", - "tags": ["Company"], + "tags": ["Restore"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json b/plugins/Redstor/v1/dataStreams/companyRestores.json similarity index 74% rename from plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json rename to plugins/Redstor/v1/dataStreams/companyRestores.json index be7bd295..7f40d105 100644 --- a/plugins/Redstor/v1/dataStreams/companyAccountRestoreStatus.json +++ b/plugins/Redstor/v1/dataStreams/companyRestores.json @@ -1,8 +1,8 @@ { - "name": "companyAccountRestoreStatus", - "displayName": "Company Account Restore Status", - "description": "Current restore operation status per account for a company", - "tags": ["Account"], + "name": "companyRestores", + "displayName": "Company Restores", + "description": "Latest restore run per account for a company (the full seven day history is available as an option, where Redstor holds one)", + "tags": ["Restore"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", @@ -14,9 +14,18 @@ "in": { "realm": "payload", "path": "nextPageToken" }, "out": { "realm": "queryArg", "path": "pageToken" } }, - "pathToData": "results" + "postRequestScript": "companyRestores.js" }, "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, + "ui": [ + { + "type": "checkbox", + "name": "includeHistory", + "label": "Include full history", + "defaultValue": false, + "help": "Returns every run in Redstor's rolling seven day window, where history is available. Large datasets may exceed the amount of data a tile can return." + } + ], "metadata": [ { "name": "accountName", "displayName": "Account", "shape": "string", "role": "label" }, { diff --git a/plugins/Redstor/v1/dataStreams/companySubscriptions.json b/plugins/Redstor/v1/dataStreams/companySubscriptions.json index e545b0ee..034ae330 100644 --- a/plugins/Redstor/v1/dataStreams/companySubscriptions.json +++ b/plugins/Redstor/v1/dataStreams/companySubscriptions.json @@ -2,7 +2,6 @@ "name": "companySubscriptions", "displayName": "Company Subscriptions", "description": "Product subscriptions for a company", - "tags": ["Company"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/productEditions.json b/plugins/Redstor/v1/dataStreams/productEditions.json index 0f6d82ec..03308aa9 100644 --- a/plugins/Redstor/v1/dataStreams/productEditions.json +++ b/plugins/Redstor/v1/dataStreams/productEditions.json @@ -2,7 +2,6 @@ "name": "productEditions", "displayName": "Product Editions", "description": "Editions available for each Redstor product", - "tags": ["Usage"], "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/productServices.json b/plugins/Redstor/v1/dataStreams/productServices.json index cc1b7fcd..53719a8a 100644 --- a/plugins/Redstor/v1/dataStreams/productServices.json +++ b/plugins/Redstor/v1/dataStreams/productServices.json @@ -2,7 +2,6 @@ "name": "productServices", "displayName": "Product Services", "description": "Services available within each Redstor product (Exchange and OneDrive sit within O365)", - "tags": ["Usage"], "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/products.json b/plugins/Redstor/v1/dataStreams/products.json index da14bef5..6e40df8d 100644 --- a/plugins/Redstor/v1/dataStreams/products.json +++ b/plugins/Redstor/v1/dataStreams/products.json @@ -2,7 +2,6 @@ "name": "products", "displayName": "Products", "description": "Redstor product catalog", - "tags": ["Usage"], "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/scripts/accountBackups.js b/plugins/Redstor/v1/dataStreams/scripts/accountBackups.js new file mode 100644 index 00000000..a093c8f1 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/accountBackups.js @@ -0,0 +1,12 @@ +// GET /backups/status/accounts requires companyId, with accountId as an optional extra filter on +// the same call (confirmed against RedAPI's OpenAPI spec) — so this scopes the request to a +// single account server-side instead of fetching the whole company and filtering here. +const account = data?.results?.[0]; +const history = account?.statusHistory || []; + +result = history.map((entry) => ({ + timestamp: entry.timestamp, + status: entry.status, + message: entry.message, + errorCount: entry.errorCount, +})); diff --git a/plugins/Redstor/v1/dataStreams/scripts/accountRestores.js b/plugins/Redstor/v1/dataStreams/scripts/accountRestores.js new file mode 100644 index 00000000..a9a6b5e6 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/accountRestores.js @@ -0,0 +1,16 @@ +// GET /restores/status/accounts requires companyId, with accountId as an optional extra filter on +// the same call (confirmed against RedAPI's OpenAPI spec) — so this scopes the request to a +// single account server-side instead of fetching the whole company and filtering here. Falls back +// to a single row from the account object itself if no statusHistory array is present, since +// Redstor returns no status at all for accounts with no restore activity (see README known +// limitations). +const account = data?.results?.[0]; +const history = account?.statusHistory; + +if (history) { + result = history.map((entry) => ({ timestamp: entry.timestamp, status: entry.status })); +} else if (account) { + result = [{ timestamp: account.timestamp ?? null, status: account.status ?? null }]; +} else { + result = []; +} diff --git a/plugins/Redstor/v1/dataStreams/scripts/companyAccountBackupStatus.js b/plugins/Redstor/v1/dataStreams/scripts/companyBackups.js similarity index 100% rename from plugins/Redstor/v1/dataStreams/scripts/companyAccountBackupStatus.js rename to plugins/Redstor/v1/dataStreams/scripts/companyBackups.js diff --git a/plugins/Redstor/v1/dataStreams/scripts/companyRestores.js b/plugins/Redstor/v1/dataStreams/scripts/companyRestores.js new file mode 100644 index 00000000..b769fc01 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/companyRestores.js @@ -0,0 +1,39 @@ +// GET /restores/status/accounts previously had no post-request script and read timestamp/status +// directly off each account entry, which returned account and service names but never a run date. +// /backups/status/accounts (see companyBackups.js) uses the same account-list shape but nests +// individual runs in a `statusHistory` array, so this assumes /restores/status/accounts does too +// and flattens it the same way. Where an account has no statusHistory (e.g. no restore activity — +// see README known limitations), it falls back to the account object's own fields so accounts +// still list even with nothing to show against them. +const accounts = data?.results || []; +const includeHistory = context.config.includeHistory === true; + +result = accounts.flatMap((account) => { + const history = account.statusHistory; + + if (!history) { + return [ + { + accountId: account.accountId, + accountName: account.accountName, + productId: account.productId, + companyId: account.companyId, + groupId: account.groupId, + timestamp: account.timestamp ?? null, + status: account.status ?? null, + } + ]; + } + + const entries = includeHistory ? history : _.take(_.orderBy(history, ["timestamp"], ["desc"]), 1); + + return entries.map((entry) => ({ + accountId: account.accountId, + accountName: account.accountName, + productId: account.productId, + companyId: account.companyId, + groupId: account.groupId, + timestamp: entry.timestamp, + status: entry.status, + })); +}); diff --git a/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json index 7c7ad03f..1d4dd126 100644 --- a/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json +++ b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json @@ -192,6 +192,82 @@ } } } + }, + { + "i": "2086c180-4d1e-48eb-ace2-6b38858b1e4b", + "x": 0, + "y": 10, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Backups", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Account]}}"], + "dataStream": { + "id": "{{dataStreams.accountBackups}}", + "name": "accountBackups", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["timestamp", "desc"]] } + }, + "scope": { + "scope": "{{scopes.[Accounts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Account]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["timestamp", "overallStatus", "message", "errorCount"] + } + } + } + } + }, + { + "i": "388195c4-3020-4e54-a3a2-2d5dda9e913d", + "x": 2, + "y": 10, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Restores", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Account]}}"], + "dataStream": { + "id": "{{dataStreams.accountRestores}}", + "name": "accountRestores", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["timestamp", "desc"]] } + }, + "scope": { + "scope": "{{scopes.[Accounts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Account]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["timestamp", "status"] + } + } + } + } } ] } diff --git a/plugins/Redstor/v1/defaultContent/overview.dash.json b/plugins/Redstor/v1/defaultContent/overview.dash.json index 264a4e05..ea946f28 100644 --- a/plugins/Redstor/v1/defaultContent/overview.dash.json +++ b/plugins/Redstor/v1/defaultContent/overview.dash.json @@ -63,8 +63,8 @@ "timeframe": "none", "variables": ["{{variables.[Company]}}"], "dataStream": { - "id": "{{dataStreams.companyBackupStatus}}", - "name": "companyBackupStatus", + "id": "{{dataStreams.companyBackupSummary}}", + "name": "companyBackupSummary", "pluginConfigId": "{{configId}}", "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["failed"] }] } }, @@ -101,8 +101,8 @@ "timeframe": "none", "variables": ["{{variables.[Company]}}"], "dataStream": { - "id": "{{dataStreams.companyRestoreStatus}}", - "name": "companyRestoreStatus", + "id": "{{dataStreams.companyRestoreSummary}}", + "name": "companyRestoreSummary", "pluginConfigId": "{{configId}}", "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["failed"] }] } }, @@ -177,8 +177,8 @@ "timeframe": "none", "variables": ["{{variables.[Company]}}"], "dataStream": { - "id": "{{dataStreams.companyBackupStatus}}", - "name": "companyBackupStatus", + "id": "{{dataStreams.companyBackupSummary}}", + "name": "companyBackupSummary", "pluginConfigId": "{{configId}}" }, "scope": { @@ -224,8 +224,8 @@ "timeframe": "none", "variables": ["{{variables.[Company]}}"], "dataStream": { - "id": "{{dataStreams.companyRestoreStatus}}", - "name": "companyRestoreStatus", + "id": "{{dataStreams.companyRestoreSummary}}", + "name": "companyRestoreSummary", "pluginConfigId": "{{configId}}" }, "scope": { @@ -376,6 +376,82 @@ } } } + }, + { + "i": "4f80d3dc-2196-4cc2-8cdf-e558c3cf73b3", + "x": 0, + "y": 14, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Backup Errors", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companyBackupErrors}}", + "name": "companyBackupErrors", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["timestamp", "desc"]] } + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["accountName", "timestamp", "message", "category", "type", "count"] + } + } + } + } + }, + { + "i": "9f9d1c81-eadf-4bea-b50e-cf082226d344", + "x": 2, + "y": 14, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Restore Errors", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "variables": ["{{variables.[Company]}}"], + "dataStream": { + "id": "{{dataStreams.companyRestoreErrors}}", + "name": "companyRestoreErrors", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["timestamp", "desc"]] } + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["accountName", "timestamp", "message", "category", "type", "count"] + } + } + } + } } ] } diff --git a/plugins/Redstor/v1/docs/README.md b/plugins/Redstor/v1/docs/README.md index 9da14996..fe9abfd6 100644 --- a/plugins/Redstor/v1/docs/README.md +++ b/plugins/Redstor/v1/docs/README.md @@ -39,11 +39,15 @@ The out-of-the-box dashboards include a Company **Overview** and a perspective f - **Company Consumption** — seat counts, licensed users, data protected and fair use overage by product, for a company. - **Company Subscriptions** — product subscriptions for a company, with the product and edition named and the trial status shown. -- **Company Backup Status** — backup results by product for a company, counting succeeded, warnings, errors, failures and missed runs. -- **Company Restore Status** — restore results by product for a company. -- **Company Account Backup Status** — the latest backup run for every account in a company, with an option to return the full history Redstor holds. -- **Company Account Restore Status** — the latest restore state for every account in a company. +- **Company Backup Summary** — backup results by product for a company, counting succeeded, warnings, errors, failures and missed runs. +- **Company Restore Summary** — restore results by product for a company. +- **Company Backups** — the latest backup run for every account in a company, with an option to return the full history Redstor holds. +- **Company Restores** — the latest restore state for every account in a company. - **Company Accounts** — every backup account in a company, one row per account, with its product, service, storage region and creation date. +- **Account Backups** — backup run history for a single account, over Redstor's rolling seven day window. +- **Account Restores** — restore run history for a single account, where Redstor holds one. +- **Company Backup Errors** — backup error and warning messages for every account in a company. +- **Company Restore Errors** — restore error and warning messages for every account in a company. - **Account Backup Errors** — backup error and warning messages for a single account. - **Account Restore Errors** — restore error and warning messages for a single account. - **Products** — the Redstor product catalog. @@ -67,7 +71,7 @@ Seats are not indexed as objects, because Redstor reports them as counts rather ## Known limitations - **No historical data** — every stream reports the current state, as RedAPI exposes no time ranges, so tiles have no timeframe selection. Consumption is the one partial exception: it can report a single past date, but not a range or a trend. -- **Backup history is limited and can be large** — Company Account Backup Status shows the latest run per account by default. Enabling **Include full history** returns every run in Redstor's rolling seven day window, which on a large customer can be more data than a single tile can return. +- **Backup and restore history is limited and can be large** — Company Backups and Company Restores show the latest run per account by default. Enabling **Include full history** returns every run in Redstor's rolling seven day window, which on a large customer can be more data than a single tile can return. Account Backups and Account Restores always return the full history, since they cover one account at a time. - **Accounts that have never run a backup have no error detail** — Redstor only keeps error messages for accounts that have backup history. The Account Backup Errors and Account Restore Errors streams say so rather than returning rows. In testing this applied to roughly a third of a company's accounts. - **Machine products report workloads, not seats** — Machines and Azure Virtual Machines return zero for every seat count and are measured by workload count instead, so seat-based tiles will look empty for those products. - **No billable seat total** — Redstor reports active, inactive and shared seats separately but publishes no chargeable figure, so billing has to be worked out from your own agreement and Redstor's fair use rules rather than read directly. From 98d491f0331b82389c33db75ef6290386dbb02a6 Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:25:05 +0100 Subject: [PATCH 08/24] Revert Company Backups to its original form and drop redundant scripts - Revert Company Backups to the flat passthrough it was at 699e383, keeping only the current name and tag. The per-row serviceName objectPropertyPath lookup and overallStatus expression were added after the version that performed, and the stream now times out while returning ~1/7th the rows the original did, so per-row work rather than row count is the likely cause - Drop the post-request scripts from Company Restores and Account Restores. RedAPI returns OperationStatusByAccount for /restores/status/accounts, whose fields are flat - there is no statusHistory to flatten, so pathToData alone covers it and the scripts only ever hit their fallback branch - Remove the includeHistory option from Company Restores, which never applied - Remove the orphaned Backup/Restore Errors tiles from the Overview dashboard, left behind when those streams were deleted. squaredup validate does not catch dangling {{dataStreams.x}} references, so this shipped unnoticed Co-Authored-By: Claude Opus 5 (1M context) --- .../v1/dataStreams/accountRestores.json | 9 ++- .../v1/dataStreams/companyBackups.json | 28 +------ .../v1/dataStreams/companyRestores.json | 13 +--- .../v1/dataStreams/scripts/accountRestores.js | 16 ---- .../v1/dataStreams/scripts/companyBackups.js | 22 ++---- .../v1/dataStreams/scripts/companyRestores.js | 39 ---------- .../v1/defaultContent/overview.dash.json | 76 ------------------- 7 files changed, 17 insertions(+), 186 deletions(-) delete mode 100644 plugins/Redstor/v1/dataStreams/scripts/accountRestores.js delete mode 100644 plugins/Redstor/v1/dataStreams/scripts/companyRestores.js diff --git a/plugins/Redstor/v1/dataStreams/accountRestores.json b/plugins/Redstor/v1/dataStreams/accountRestores.json index 8edcbba2..d2818ed5 100644 --- a/plugins/Redstor/v1/dataStreams/accountRestores.json +++ b/plugins/Redstor/v1/dataStreams/accountRestores.json @@ -1,7 +1,7 @@ { "name": "accountRestores", "displayName": "Account Restores", - "description": "Restore run history for an account, over Redstor's rolling seven day window (where Redstor holds one)", + "description": "Current restore operation status for an account", "tags": ["Restore"], "baseDataSourceName": "httpRequestScopedSingle", "config": { @@ -17,7 +17,7 @@ "in": { "realm": "payload", "path": "nextPageToken" }, "out": { "realm": "queryArg", "path": "pageToken" } }, - "postRequestScript": "accountRestores.js" + "pathToData": "results" }, "matches": { "sourceType": { "type": "oneOf", "values": ["Account"] } }, "metadata": [ @@ -27,7 +27,10 @@ "displayName": "Status Code", "shape": "number", "help": "Raw Redstor status code — RedAPI's public documentation does not define the meaning of each value." - } + }, + { "name": "accountName", "displayName": "Account", "shape": "string", "visible": false }, + { "name": "accountId", "displayName": "Account ID", "shape": "guid", "visible": false }, + { "name": "productId", "displayName": "Service ID", "shape": "number", "visible": false } ], "timeframes": false } diff --git a/plugins/Redstor/v1/dataStreams/companyBackups.json b/plugins/Redstor/v1/dataStreams/companyBackups.json index c4a7d370..4032bee1 100644 --- a/plugins/Redstor/v1/dataStreams/companyBackups.json +++ b/plugins/Redstor/v1/dataStreams/companyBackups.json @@ -1,7 +1,7 @@ { "name": "companyBackups", "displayName": "Company Backups", - "description": "Latest backup run per account for a company (the full seven day history is available as an option)", + "description": "Recent backup operation status history per account for a company", "tags": ["Backup"], "baseDataSourceName": "httpRequestScopedSingle", "config": { @@ -17,42 +17,18 @@ "postRequestScript": "companyBackups.js" }, "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, - "ui": [ - { - "type": "checkbox", - "name": "includeHistory", - "label": "Include full history", - "defaultValue": false, - "help": "Returns every run in Redstor's rolling seven day window. Large datasets may exceed the amount of data a tile can return." - } - ], "metadata": [ { "name": "accountName", "displayName": "Account", "shape": "string", "role": "label" }, - { "name": "serviceId", "displayName": "Service ID", "shape": "number", "visible": false }, - { - "name": "serviceName", - "displayName": "Service", - "sourceId": "serviceId", - "sourceType": "Service", - "objectPropertyPath": "name" - }, { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, - { - "name": "overallStatus", - "displayName": "Status", - "computed": true, - "valueExpression": "{{ $['status'] === 1 ? 'success' : ($['status'] === 2 ? 'warning' : 'error') }}", - "shape": "state" - }, { "name": "status", "displayName": "Status Code", "shape": "number", - "visible": false, "help": "Raw Redstor status code: 0 = Failed, 1 = Completed successfully, 2 = Completed with warnings, 3 = Completed with errors." }, { "name": "message", "displayName": "Message", "shape": "string" }, { "name": "errorCount", "displayName": "Error Count", "shape": "number" }, + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, { "name": "accountId", "displayName": "Account ID", "shape": "guid", "visible": false } ], "timeframes": false diff --git a/plugins/Redstor/v1/dataStreams/companyRestores.json b/plugins/Redstor/v1/dataStreams/companyRestores.json index 7f40d105..852e5afd 100644 --- a/plugins/Redstor/v1/dataStreams/companyRestores.json +++ b/plugins/Redstor/v1/dataStreams/companyRestores.json @@ -1,7 +1,7 @@ { "name": "companyRestores", "displayName": "Company Restores", - "description": "Latest restore run per account for a company (the full seven day history is available as an option, where Redstor holds one)", + "description": "Current restore operation status per account for a company", "tags": ["Restore"], "baseDataSourceName": "httpRequestScopedSingle", "config": { @@ -14,18 +14,9 @@ "in": { "realm": "payload", "path": "nextPageToken" }, "out": { "realm": "queryArg", "path": "pageToken" } }, - "postRequestScript": "companyRestores.js" + "pathToData": "results" }, "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, - "ui": [ - { - "type": "checkbox", - "name": "includeHistory", - "label": "Include full history", - "defaultValue": false, - "help": "Returns every run in Redstor's rolling seven day window, where history is available. Large datasets may exceed the amount of data a tile can return." - } - ], "metadata": [ { "name": "accountName", "displayName": "Account", "shape": "string", "role": "label" }, { diff --git a/plugins/Redstor/v1/dataStreams/scripts/accountRestores.js b/plugins/Redstor/v1/dataStreams/scripts/accountRestores.js deleted file mode 100644 index a9a6b5e6..00000000 --- a/plugins/Redstor/v1/dataStreams/scripts/accountRestores.js +++ /dev/null @@ -1,16 +0,0 @@ -// GET /restores/status/accounts requires companyId, with accountId as an optional extra filter on -// the same call (confirmed against RedAPI's OpenAPI spec) — so this scopes the request to a -// single account server-side instead of fetching the whole company and filtering here. Falls back -// to a single row from the account object itself if no statusHistory array is present, since -// Redstor returns no status at all for accounts with no restore activity (see README known -// limitations). -const account = data?.results?.[0]; -const history = account?.statusHistory; - -if (history) { - result = history.map((entry) => ({ timestamp: entry.timestamp, status: entry.status })); -} else if (account) { - result = [{ timestamp: account.timestamp ?? null, status: account.status ?? null }]; -} else { - result = []; -} diff --git a/plugins/Redstor/v1/dataStreams/scripts/companyBackups.js b/plugins/Redstor/v1/dataStreams/scripts/companyBackups.js index d43039eb..f1843e0d 100644 --- a/plugins/Redstor/v1/dataStreams/scripts/companyBackups.js +++ b/plugins/Redstor/v1/dataStreams/scripts/companyBackups.js @@ -1,23 +1,15 @@ -// GET /backups/status/accounts returns one entry per account, each carrying a -// `statusHistory` array covering a fixed rolling seven day window. Flattening every -// run produces roughly seven rows per account, which on a large company approaches -// the response size limit, so only the latest run per account is kept by default. -// The endpoint's `productId` field actually carries service ids (94 = Exchange, -// 91 = OneDrive, 92 = SharePoint, 107 = Teams), not the product ids used elsewhere. +// GET /backups/status/accounts returns one entry per account, each carrying +// a `statusHistory` array of recent runs — flatten to one row per run. const accounts = data?.results || []; -const includeHistory = context.config.includeHistory === true; -result = accounts.flatMap((account) => { - const history = account.statusHistory || []; - const entries = includeHistory ? history : _.take(_.orderBy(history, ["timestamp"], ["desc"]), 1); - - return entries.map((entry) => ({ +result = accounts.flatMap((account) => + (account.statusHistory || []).map((entry) => ({ accountId: account.accountId, accountName: account.accountName, - serviceId: account.productId, + productId: account.productId, timestamp: entry.timestamp, status: entry.status, message: entry.message, errorCount: entry.errorCount, - })); -}); + })), +); diff --git a/plugins/Redstor/v1/dataStreams/scripts/companyRestores.js b/plugins/Redstor/v1/dataStreams/scripts/companyRestores.js deleted file mode 100644 index b769fc01..00000000 --- a/plugins/Redstor/v1/dataStreams/scripts/companyRestores.js +++ /dev/null @@ -1,39 +0,0 @@ -// GET /restores/status/accounts previously had no post-request script and read timestamp/status -// directly off each account entry, which returned account and service names but never a run date. -// /backups/status/accounts (see companyBackups.js) uses the same account-list shape but nests -// individual runs in a `statusHistory` array, so this assumes /restores/status/accounts does too -// and flattens it the same way. Where an account has no statusHistory (e.g. no restore activity — -// see README known limitations), it falls back to the account object's own fields so accounts -// still list even with nothing to show against them. -const accounts = data?.results || []; -const includeHistory = context.config.includeHistory === true; - -result = accounts.flatMap((account) => { - const history = account.statusHistory; - - if (!history) { - return [ - { - accountId: account.accountId, - accountName: account.accountName, - productId: account.productId, - companyId: account.companyId, - groupId: account.groupId, - timestamp: account.timestamp ?? null, - status: account.status ?? null, - } - ]; - } - - const entries = includeHistory ? history : _.take(_.orderBy(history, ["timestamp"], ["desc"]), 1); - - return entries.map((entry) => ({ - accountId: account.accountId, - accountName: account.accountName, - productId: account.productId, - companyId: account.companyId, - groupId: account.groupId, - timestamp: entry.timestamp, - status: entry.status, - })); -}); diff --git a/plugins/Redstor/v1/defaultContent/overview.dash.json b/plugins/Redstor/v1/defaultContent/overview.dash.json index ea946f28..e47312a8 100644 --- a/plugins/Redstor/v1/defaultContent/overview.dash.json +++ b/plugins/Redstor/v1/defaultContent/overview.dash.json @@ -376,82 +376,6 @@ } } } - }, - { - "i": "4f80d3dc-2196-4cc2-8cdf-e558c3cf73b3", - "x": 0, - "y": 14, - "w": 2, - "h": 4, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Backup Errors", - "description": "", - "activePluginConfigIds": ["{{configId}}"], - "timeframe": "none", - "variables": ["{{variables.[Company]}}"], - "dataStream": { - "id": "{{dataStreams.companyBackupErrors}}", - "name": "companyBackupErrors", - "pluginConfigId": "{{configId}}", - "sort": { "by": [["timestamp", "desc"]] } - }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": ["accountName", "timestamp", "message", "category", "type", "count"] - } - } - } - } - }, - { - "i": "9f9d1c81-eadf-4bea-b50e-cf082226d344", - "x": 2, - "y": 14, - "w": 2, - "h": 4, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Restore Errors", - "description": "", - "activePluginConfigIds": ["{{configId}}"], - "timeframe": "none", - "variables": ["{{variables.[Company]}}"], - "dataStream": { - "id": "{{dataStreams.companyRestoreErrors}}", - "name": "companyRestoreErrors", - "pluginConfigId": "{{configId}}", - "sort": { "by": [["timestamp", "desc"]] } - }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": ["accountName", "timestamp", "message", "category", "type", "count"] - } - } - } - } } ] } From 372ea06f4353e8ab2535a04b1a2fc5d414bde49c Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:53:48 +0100 Subject: [PATCH 09/24] Surface error detail on account error streams and clear API detail from help - Show the extraInfo field on Account Backup/Restore Errors as Error Message, and add a Service column resolving the id via a lookup in the JSON. That id arrives on a field RedAPI calls productId but which holds service ids, so it resolves against Service, not Product - Remove API implementation detail from user-facing help text across the streams. Help renders as a tooltip in product, so upstream field names and notes about the vendor's API docs do not belong there. Four of these sat on hidden columns, where no one could ever have read them Co-Authored-By: Claude Opus 5 (1M context) --- .../v1/dataStreams/accountBackupErrors.json | 11 +++++++++-- .../Redstor/v1/dataStreams/accountBackups.json | 8 +------- .../v1/dataStreams/accountRestoreErrors.json | 11 +++++++++-- .../Redstor/v1/dataStreams/accountRestores.json | 7 +------ .../Redstor/v1/dataStreams/companyBackups.json | 2 +- .../Redstor/v1/dataStreams/companyRestores.json | 15 ++------------- 6 files changed, 23 insertions(+), 31 deletions(-) diff --git a/plugins/Redstor/v1/dataStreams/accountBackupErrors.json b/plugins/Redstor/v1/dataStreams/accountBackupErrors.json index 285589f5..54318a37 100644 --- a/plugins/Redstor/v1/dataStreams/accountBackupErrors.json +++ b/plugins/Redstor/v1/dataStreams/accountBackupErrors.json @@ -24,8 +24,15 @@ { "name": "category", "displayName": "Category", "shape": "string" }, { "name": "type", "displayName": "Type", "shape": "string" }, { "name": "count", "displayName": "Count", "shape": "number" }, - { "name": "extraInfo", "displayName": "Extra Info", "shape": "string", "visible": false }, - { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false } + { "name": "extraInfo", "displayName": "Error Message", "shape": "string" }, + { "name": "productId", "displayName": "Service ID", "shape": "number", "visible": false }, + { + "name": "serviceName", + "displayName": "Service", + "sourceId": "productId", + "sourceType": "Service", + "objectPropertyPath": "name" + } ], "timeframes": false } diff --git a/plugins/Redstor/v1/dataStreams/accountBackups.json b/plugins/Redstor/v1/dataStreams/accountBackups.json index 8d47f690..03d60ec6 100644 --- a/plugins/Redstor/v1/dataStreams/accountBackups.json +++ b/plugins/Redstor/v1/dataStreams/accountBackups.json @@ -29,13 +29,7 @@ "valueExpression": "{{ $['status'] === 1 ? 'success' : ($['status'] === 2 ? 'warning' : 'error') }}", "shape": "state" }, - { - "name": "status", - "displayName": "Status Code", - "shape": "number", - "visible": false, - "help": "Raw Redstor status code: 0 = Failed, 1 = Completed successfully, 2 = Completed with warnings, 3 = Completed with errors." - }, + { "name": "status", "displayName": "Status Code", "shape": "number", "visible": false }, { "name": "message", "displayName": "Message", "shape": "string", "role": "label" }, { "name": "errorCount", "displayName": "Error Count", "shape": "number" } ], diff --git a/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json b/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json index ead9da90..918f813a 100644 --- a/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json +++ b/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json @@ -24,8 +24,15 @@ { "name": "category", "displayName": "Category", "shape": "string" }, { "name": "type", "displayName": "Type", "shape": "string" }, { "name": "count", "displayName": "Count", "shape": "number" }, - { "name": "extraInfo", "displayName": "Extra Info", "shape": "string", "visible": false }, - { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false } + { "name": "extraInfo", "displayName": "Error Message", "shape": "string" }, + { "name": "productId", "displayName": "Service ID", "shape": "number", "visible": false }, + { + "name": "serviceName", + "displayName": "Service", + "sourceId": "productId", + "sourceType": "Service", + "objectPropertyPath": "name" + } ], "timeframes": false } diff --git a/plugins/Redstor/v1/dataStreams/accountRestores.json b/plugins/Redstor/v1/dataStreams/accountRestores.json index d2818ed5..9a270309 100644 --- a/plugins/Redstor/v1/dataStreams/accountRestores.json +++ b/plugins/Redstor/v1/dataStreams/accountRestores.json @@ -22,12 +22,7 @@ "matches": { "sourceType": { "type": "oneOf", "values": ["Account"] } }, "metadata": [ { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, - { - "name": "status", - "displayName": "Status Code", - "shape": "number", - "help": "Raw Redstor status code — RedAPI's public documentation does not define the meaning of each value." - }, + { "name": "status", "displayName": "Status Code", "shape": "number" }, { "name": "accountName", "displayName": "Account", "shape": "string", "visible": false }, { "name": "accountId", "displayName": "Account ID", "shape": "guid", "visible": false }, { "name": "productId", "displayName": "Service ID", "shape": "number", "visible": false } diff --git a/plugins/Redstor/v1/dataStreams/companyBackups.json b/plugins/Redstor/v1/dataStreams/companyBackups.json index 4032bee1..551605b5 100644 --- a/plugins/Redstor/v1/dataStreams/companyBackups.json +++ b/plugins/Redstor/v1/dataStreams/companyBackups.json @@ -24,7 +24,7 @@ "name": "status", "displayName": "Status Code", "shape": "number", - "help": "Raw Redstor status code: 0 = Failed, 1 = Completed successfully, 2 = Completed with warnings, 3 = Completed with errors." + "help": "0 = Failed, 1 = Completed successfully, 2 = Completed with warnings, 3 = Completed with errors" }, { "name": "message", "displayName": "Message", "shape": "string" }, { "name": "errorCount", "displayName": "Error Count", "shape": "number" }, diff --git a/plugins/Redstor/v1/dataStreams/companyRestores.json b/plugins/Redstor/v1/dataStreams/companyRestores.json index 852e5afd..a268d57e 100644 --- a/plugins/Redstor/v1/dataStreams/companyRestores.json +++ b/plugins/Redstor/v1/dataStreams/companyRestores.json @@ -27,19 +27,8 @@ "objectPropertyPath": "name" }, { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, - { - "name": "status", - "displayName": "Status Code", - "shape": "number", - "help": "Raw Redstor status code — RedAPI's public documentation does not define the meaning of each value." - }, - { - "name": "productId", - "displayName": "Service ID", - "shape": "number", - "visible": false, - "help": "RedAPI names this field productId, but it carries service ids (94 = Exchange, 91 = OneDrive, 92 = SharePoint, 107 = Teams)" - }, + { "name": "status", "displayName": "Status Code", "shape": "number" }, + { "name": "productId", "displayName": "Service ID", "shape": "number", "visible": false }, { "name": "accountId", "displayName": "Account ID", "shape": "guid", "visible": false }, { "name": "companyId", "displayName": "Company ID", "shape": "guid", "visible": false }, { "name": "groupId", "displayName": "Group ID", "shape": "string", "visible": false } From b0bebb0f60b2921db6d3211a5e29c9b38e181aac Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Sat, 12 Sep 2026 08:37:08 +0100 Subject: [PATCH 10/24] Add companyBackupRollupSummary stream for /backups/status/summary Returns per-product backup status split between the company's own backups and its customer companies' rolled-up backups, for comparison against the existing status-summary streams. Co-Authored-By: Claude Sonnet 5 --- .../companyBackupRollupSummary.json | 32 +++++++++++++++++++ .../scripts/companyBackupRollupSummary.js | 6 ++++ 2 files changed, 38 insertions(+) create mode 100644 plugins/Redstor/v1/dataStreams/companyBackupRollupSummary.json create mode 100644 plugins/Redstor/v1/dataStreams/scripts/companyBackupRollupSummary.js diff --git a/plugins/Redstor/v1/dataStreams/companyBackupRollupSummary.json b/plugins/Redstor/v1/dataStreams/companyBackupRollupSummary.json new file mode 100644 index 00000000..23668396 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companyBackupRollupSummary.json @@ -0,0 +1,32 @@ +{ + "name": "companyBackupRollupSummary", + "displayName": "Company Backup Rollup Summary", + "description": "Current backup status by product, split between the company's own backups and its customer companies' rolled-up backups", + "tags": ["Backup"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/backups/status/summary", + "getArgs": [{ "key": "companyId", "value": "{{object.rawId}}" }], + "postRequestScript": "companyBackupRollupSummary.js" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, + "metadata": [ + { "name": "productName", "displayName": "Product", "shape": "string", "role": "label" }, + { "name": "scope", "displayName": "Scope", "shape": "string" }, + { "name": "succeeded", "displayName": "Succeeded", "shape": "number" }, + { "name": "completedWithWarnings", "displayName": "Completed with Warnings", "shape": "number" }, + { "name": "completedWithErrors", "displayName": "Completed with Errors", "shape": "number" }, + { "name": "failed", "displayName": "Failed", "shape": "number" }, + { "name": "missed", "displayName": "Missed", "shape": "number" }, + { + "name": "overallStatus", + "displayName": "Status", + "computed": true, + "valueExpression": "{{ $['failed'] > 0 ? 'error' : (($['completedWithErrors'] > 0 || $['missed'] > 0) ? 'warning' : 'success') }}", + "shape": "state" + }, + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/scripts/companyBackupRollupSummary.js b/plugins/Redstor/v1/dataStreams/scripts/companyBackupRollupSummary.js new file mode 100644 index 00000000..85224ac9 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/companyBackupRollupSummary.js @@ -0,0 +1,6 @@ +// GET /backups/status/summary returns two arrays for the queried companyId: "companySummary" +// (that company's own backups) and "customerSummary" (rolled up across its customer companies, +// when the company is a Redstor Partner) — tag each row with which one it came from. +const tag = (rows, scope) => (rows || []).map((row) => ({ ...row, scope })); + +result = [...tag(data.companySummary, "This company"), ...tag(data.customerSummary, "Customer companies")]; From 961983d2cb6b9ca0d85dbd0f05d14437ea54ad51 Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Sat, 12 Sep 2026 22:23:17 +0100 Subject: [PATCH 11/24] Remove companyBackupRollupSummary data stream Tested against the live tenant but the endpoint's per-call cost doesn't beat the existing companyBackupSummary/companyBackups streams, so not carrying it forward. Co-Authored-By: Claude Sonnet 5 --- .../companyBackupRollupSummary.json | 32 ------------------- .../scripts/companyBackupRollupSummary.js | 6 ---- 2 files changed, 38 deletions(-) delete mode 100644 plugins/Redstor/v1/dataStreams/companyBackupRollupSummary.json delete mode 100644 plugins/Redstor/v1/dataStreams/scripts/companyBackupRollupSummary.js diff --git a/plugins/Redstor/v1/dataStreams/companyBackupRollupSummary.json b/plugins/Redstor/v1/dataStreams/companyBackupRollupSummary.json deleted file mode 100644 index 23668396..00000000 --- a/plugins/Redstor/v1/dataStreams/companyBackupRollupSummary.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "companyBackupRollupSummary", - "displayName": "Company Backup Rollup Summary", - "description": "Current backup status by product, split between the company's own backups and its customer companies' rolled-up backups", - "tags": ["Backup"], - "baseDataSourceName": "httpRequestScopedSingle", - "config": { - "httpMethod": "get", - "endpointPath": "/backups/status/summary", - "getArgs": [{ "key": "companyId", "value": "{{object.rawId}}" }], - "postRequestScript": "companyBackupRollupSummary.js" - }, - "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, - "metadata": [ - { "name": "productName", "displayName": "Product", "shape": "string", "role": "label" }, - { "name": "scope", "displayName": "Scope", "shape": "string" }, - { "name": "succeeded", "displayName": "Succeeded", "shape": "number" }, - { "name": "completedWithWarnings", "displayName": "Completed with Warnings", "shape": "number" }, - { "name": "completedWithErrors", "displayName": "Completed with Errors", "shape": "number" }, - { "name": "failed", "displayName": "Failed", "shape": "number" }, - { "name": "missed", "displayName": "Missed", "shape": "number" }, - { - "name": "overallStatus", - "displayName": "Status", - "computed": true, - "valueExpression": "{{ $['failed'] > 0 ? 'error' : (($['completedWithErrors'] > 0 || $['missed'] > 0) ? 'warning' : 'success') }}", - "shape": "state" - }, - { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false } - ], - "timeframes": false -} diff --git a/plugins/Redstor/v1/dataStreams/scripts/companyBackupRollupSummary.js b/plugins/Redstor/v1/dataStreams/scripts/companyBackupRollupSummary.js deleted file mode 100644 index 85224ac9..00000000 --- a/plugins/Redstor/v1/dataStreams/scripts/companyBackupRollupSummary.js +++ /dev/null @@ -1,6 +0,0 @@ -// GET /backups/status/summary returns two arrays for the queried companyId: "companySummary" -// (that company's own backups) and "customerSummary" (rolled up across its customer companies, -// when the company is a Redstor Partner) — tag each row with which one it came from. -const tag = (rows, scope) => (rows || []).map((row) => ({ ...row, scope })); - -result = [...tag(data.companySummary, "This company"), ...tag(data.customerSummary, "Customer companies")]; From 0e78a072d8abab58288f7b95b9530f225e15b8eb Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:47:50 +0100 Subject: [PATCH 12/24] Pre-PR polish: tags, un-hide Company Subscriptions, README/ui.json wording - Add missing tags to Company Accounts (Accounts) and Company Subscriptions (Usage). - Un-hide Company Subscriptions - it backs a tile on the Overview dashboard, so it should be user-pickable too. - Rewrite the README's Data streams section: explain when to use Company vs Account streams, drop streams that no longer exist or are hidden, fix the stale "Include full history" limitation. - ui.json: fix help text to start with a verb, fix "Private Key" double-capitalisation (README updated to match). - metadata.json: drop "RedAPI" from the description per REVIEW.md's guidance against API/implementation language. Co-Authored-By: Claude Sonnet 5 --- .../v1/dataStreams/backupAccounts.json | 1 + .../v1/dataStreams/companySubscriptions.json | 2 +- plugins/Redstor/v1/docs/README.md | 29 ++++++++++--------- plugins/Redstor/v1/metadata.json | 2 +- plugins/Redstor/v1/ui.json | 6 ++-- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/plugins/Redstor/v1/dataStreams/backupAccounts.json b/plugins/Redstor/v1/dataStreams/backupAccounts.json index 80489f24..80107224 100644 --- a/plugins/Redstor/v1/dataStreams/backupAccounts.json +++ b/plugins/Redstor/v1/dataStreams/backupAccounts.json @@ -2,6 +2,7 @@ "name": "backupAccounts", "displayName": "Company Accounts", "description": "Backup accounts for a company (one row per account, and a single seat can hold more than one)", + "tags": ["Accounts"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", diff --git a/plugins/Redstor/v1/dataStreams/companySubscriptions.json b/plugins/Redstor/v1/dataStreams/companySubscriptions.json index 034ae330..04344088 100644 --- a/plugins/Redstor/v1/dataStreams/companySubscriptions.json +++ b/plugins/Redstor/v1/dataStreams/companySubscriptions.json @@ -2,6 +2,7 @@ "name": "companySubscriptions", "displayName": "Company Subscriptions", "description": "Product subscriptions for a company", + "tags": ["Usage"], "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", @@ -16,7 +17,6 @@ "pathToData": "results" }, "matches": { "sourceType": { "type": "oneOf", "values": ["Company"] } }, - "visibility": { "type": "hidden" }, "metadata": [ { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, { "name": "editionId", "displayName": "Edition ID", "shape": "number", "visible": false }, diff --git a/plugins/Redstor/v1/docs/README.md b/plugins/Redstor/v1/docs/README.md index fe9abfd6..d5f37cbb 100644 --- a/plugins/Redstor/v1/docs/README.md +++ b/plugins/Redstor/v1/docs/README.md @@ -11,7 +11,7 @@ You will need a RedAPI **service account** with its Client ID and private key, p 3. Go to **RedAPI → Service accounts** and click to add a new service account. Give it a descriptive name and assign it access to your Partner company (and the customers you want visibility into). 4. Under the service account, create a **key**. Redstor generates a Client ID and a JSON Web Key (JWK) file — download the JWK file. 5. Copy the **Client ID** into the **Client ID** field. -6. Open the downloaded JWK file (a `.json` file) and paste its full contents, exactly as downloaded, into the **Private Key** field. +6. Open the downloaded JWK file (a `.json` file) and paste its full contents, exactly as downloaded, into the **Private key** field. ## Configuration fields @@ -19,9 +19,9 @@ You will need a RedAPI **service account** with its Client ID and private key, p | ----- | ---------- | ---------------- | -------- | | **Company ID** | Your Redstor Partner company's ID. Scopes every API call to this company and its direct customers. | RedApp → **Company Settings**. | Yes | | **Client ID** | Identifies the RedAPI service account used to authenticate. | RedApp → **RedAPI → Service accounts** → your service account's key. | Yes | -| **Private Key** | The JSON Web Key (JWK) paired with the Client ID; signs the request used to obtain access tokens. | Downloaded when the service account's key was created. | Yes | +| **Private key** | The JSON Web Key (JWK) paired with the Client ID; signs the request used to obtain access tokens. | Downloaded when the service account's key was created. | Yes | -On save, the plugin authenticates and calls your company's profile; an invalid Company ID, Client ID, or Private Key fails setup with an authentication error. +On save, the plugin authenticates and calls your company's profile; an invalid Company ID, Client ID, or Private key fails setup with an authentication error. ## What this plugin monitors @@ -37,22 +37,23 @@ The out-of-the-box dashboards include a Company **Overview** and a perspective f ## Data streams -- **Company Consumption** — seat counts, licensed users, data protected and fair use overage by product, for a company. -- **Company Subscriptions** — product subscriptions for a company, with the product and edition named and the trial status shown. +Start with the **Company** streams to see backup and restore health across every account in a company at a glance. When one needs investigating, switch to the matching **Account** stream for that account's full run history and error detail. + +**Company streams — for summarising activity across a company** + - **Company Backup Summary** — backup results by product for a company, counting succeeded, warnings, errors, failures and missed runs. - **Company Restore Summary** — restore results by product for a company. -- **Company Backups** — the latest backup run for every account in a company, with an option to return the full history Redstor holds. -- **Company Restores** — the latest restore state for every account in a company. +- **Company Backups** — backup run history for every account in a company. +- **Company Restores** — the current restore status for every account in a company. - **Company Accounts** — every backup account in a company, one row per account, with its product, service, storage region and creation date. +- **Company Consumption** — seat counts, licensed users, data protected and fair use overage by product, for a company. + +**Account streams — for tracking down an individual account's history and errors** + - **Account Backups** — backup run history for a single account, over Redstor's rolling seven day window. -- **Account Restores** — restore run history for a single account, where Redstor holds one. -- **Company Backup Errors** — backup error and warning messages for every account in a company. -- **Company Restore Errors** — restore error and warning messages for every account in a company. +- **Account Restores** — the current restore status for a single account. - **Account Backup Errors** — backup error and warning messages for a single account. - **Account Restore Errors** — restore error and warning messages for a single account. -- **Products** — the Redstor product catalog. -- **Product Services** — the services within each product, such as Exchange and OneDrive within O365. -- **Product Editions** — the editions available for each product, and which of them offer a trial. ## What gets indexed @@ -71,7 +72,7 @@ Seats are not indexed as objects, because Redstor reports them as counts rather ## Known limitations - **No historical data** — every stream reports the current state, as RedAPI exposes no time ranges, so tiles have no timeframe selection. Consumption is the one partial exception: it can report a single past date, but not a range or a trend. -- **Backup and restore history is limited and can be large** — Company Backups and Company Restores show the latest run per account by default. Enabling **Include full history** returns every run in Redstor's rolling seven day window, which on a large customer can be more data than a single tile can return. Account Backups and Account Restores always return the full history, since they cover one account at a time. +- **Backup history can be large** — Company Backups returns every run in Redstor's rolling seven day window for every account in the company, which on a large customer can be more data than a single tile can return. Account Backups covers the same window for one account at a time, so it stays small regardless of company size. - **Accounts that have never run a backup have no error detail** — Redstor only keeps error messages for accounts that have backup history. The Account Backup Errors and Account Restore Errors streams say so rather than returning rows. In testing this applied to roughly a third of a company's accounts. - **Machine products report workloads, not seats** — Machines and Azure Virtual Machines return zero for every seat count and are measured by workload count instead, so seat-based tiles will look empty for those products. - **No billable seat total** — Redstor reports active, inactive and shared seats separately but publishes no chargeable figure, so billing has to be worked out from your own agreement and Redstor's fair use rules rather than read directly. diff --git a/plugins/Redstor/v1/metadata.json b/plugins/Redstor/v1/metadata.json index 78a64457..93e5e5fe 100644 --- a/plugins/Redstor/v1/metadata.json +++ b/plugins/Redstor/v1/metadata.json @@ -3,7 +3,7 @@ "displayName": "Redstor", "version": "1.0.0", "author": { "name": "@TimWheeler-SQUP", "type": "community" }, - "description": "Monitor your Redstor RedAPI Partner account — customer companies, backup accounts, backup and restore status, storage consumption, and product subscriptions.", + "description": "Monitor your Redstor Partner account — customer companies, backup accounts, backup and restore status, storage consumption, and product subscriptions.", "category": "Monitoring", "type": "hybrid", "schemaVersion": "2.1", diff --git a/plugins/Redstor/v1/ui.json b/plugins/Redstor/v1/ui.json index da63360b..fd59ca94 100644 --- a/plugins/Redstor/v1/ui.json +++ b/plugins/Redstor/v1/ui.json @@ -4,7 +4,7 @@ "name": "entityIdentifier", "label": "Company ID", "placeholder": "e.g. 3fa85f64-5717-4562-b3fc-2c963f66afa6", - "help": "Your Redstor **Partner** company's ID in RedApp — found under **RedApp → Company Settings**. Every request is scoped to this company and its direct customers.", + "help": "Identifies your Redstor **Partner** company, found under **RedApp → Company Settings** — scopes every request to this company and its direct customers.", "validation": { "required": true } }, { @@ -13,13 +13,13 @@ "label": "Client ID", "allowEncryption": true, "placeholder": "Enter the RedAPI service account Client ID", - "help": "The Client ID of the RedAPI service account, created in RedApp → **RedAPI → Service accounts**.", + "help": "Identifies the RedAPI service account used to authenticate, created in RedApp → **RedAPI → Service accounts**.", "validation": { "required": true } }, { "type": "textarea", "name": "privateKey", - "label": "Private Key", + "label": "Private key", "rows": 12, "allowEncryption": true, "placeholder": "Paste the full JWK JSON downloaded from RedApp", From 438cc730aa12f7b581cd221ba00f2ea504e53c61 Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:42:41 +0100 Subject: [PATCH 13/24] Rebuild Redstor OOB dashboards into Overview and Account view Overview now shows the whole estate (Partner company plus every customer rolled up) with no dashboard variable, backed by three new data streams that wire up RedAPI's summary endpoints (estateBackupSummary, estateRestoreSummary, estateConsumption) plus a partnerSubscriptions stream for the Partner company's own subscriptions. The per-account dashboard is renamed to "Account view" for symmetry; its Account variable is unchanged. Co-Authored-By: Claude Sonnet 5 --- .../v1/dataStreams/estateBackupSummary.json | 32 +++ .../v1/dataStreams/estateConsumption.json | 37 ++++ .../v1/dataStreams/estateRestoreSummary.json | 31 +++ .../v1/dataStreams/partnerSubscriptions.json | 58 ++++++ .../scripts/estateBackupSummary.js | 7 + .../dataStreams/scripts/estateConsumption.js | 7 + .../scripts/estateRestoreSummary.js | 7 + .../accountPerspective.dash.json | 2 +- .../v1/defaultContent/overview.dash.json | 185 ++++++------------ plugins/Redstor/v1/docs/README.md | 17 +- 10 files changed, 258 insertions(+), 125 deletions(-) create mode 100644 plugins/Redstor/v1/dataStreams/estateBackupSummary.json create mode 100644 plugins/Redstor/v1/dataStreams/estateConsumption.json create mode 100644 plugins/Redstor/v1/dataStreams/estateRestoreSummary.json create mode 100644 plugins/Redstor/v1/dataStreams/partnerSubscriptions.json create mode 100644 plugins/Redstor/v1/dataStreams/scripts/estateBackupSummary.js create mode 100644 plugins/Redstor/v1/dataStreams/scripts/estateConsumption.js create mode 100644 plugins/Redstor/v1/dataStreams/scripts/estateRestoreSummary.js diff --git a/plugins/Redstor/v1/dataStreams/estateBackupSummary.json b/plugins/Redstor/v1/dataStreams/estateBackupSummary.json new file mode 100644 index 00000000..e218755a --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/estateBackupSummary.json @@ -0,0 +1,32 @@ +{ + "name": "estateBackupSummary", + "displayName": "Estate Backup Summary", + "description": "Current backup operation status by product, for the Partner company and rolled up across its customers", + "tags": ["Backup"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/backups/status/summary", + "getArgs": [{ "key": "companyId", "value": "{{dataSource.entityIdentifier}}" }], + "postRequestScript": "estateBackupSummary.js" + }, + "matches": "none", + "metadata": [ + { "name": "scope", "displayName": "Scope", "shape": "string" }, + { "name": "productName", "displayName": "Product", "shape": "string", "role": "label" }, + { "name": "succeeded", "displayName": "Succeeded", "shape": "number" }, + { "name": "completedWithWarnings", "displayName": "Completed with Warnings", "shape": "number" }, + { "name": "completedWithErrors", "displayName": "Completed with Errors", "shape": "number" }, + { "name": "failed", "displayName": "Failed", "shape": "number" }, + { "name": "missed", "displayName": "Missed", "shape": "number" }, + { + "name": "overallStatus", + "displayName": "Status", + "computed": true, + "valueExpression": "{{ $['failed'] > 0 ? 'error' : (($['completedWithErrors'] > 0 || $['missed'] > 0) ? 'warning' : 'success') }}", + "shape": "state" + }, + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/estateConsumption.json b/plugins/Redstor/v1/dataStreams/estateConsumption.json new file mode 100644 index 00000000..f7165635 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/estateConsumption.json @@ -0,0 +1,37 @@ +{ + "name": "estateConsumption", + "displayName": "Estate Consumption", + "description": "Storage consumption and seat usage, for the Partner company and rolled up across its customers", + "tags": ["Usage"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/consumption/summary", + "getArgs": [ + { "key": "companyId", "value": "{{dataSource.entityIdentifier}}" }, + { "key": "shouldExcludeTrials", "value": "{{excludeTrials || null}}" } + ], + "postRequestScript": "estateConsumption.js" + }, + "matches": "none", + "ui": [ + { + "type": "checkbox", + "name": "excludeTrials", + "label": "Exclude trial subscriptions", + "defaultValue": true + } + ], + "metadata": [ + { "name": "scope", "displayName": "Scope", "shape": "string", "role": "label" }, + { "name": "totalDataProtectedInBytes", "displayName": "Data Protected", "shape": "bytes" }, + { "name": "totalDataProtectedInBytesExternal", "displayName": "Data Protected (External)", "shape": "bytes" }, + { "name": "fupOverageInBytes", "displayName": "Fair Use Overage", "shape": "bytes" }, + { "name": "activeSeats", "displayName": "Active Seats", "shape": "number" }, + { "name": "inactiveSeats", "displayName": "Inactive Seats", "shape": "number" }, + { "name": "sharedSeats", "displayName": "Shared Seats", "shape": "number" }, + { "name": "activeWorkloads", "displayName": "Active Workloads", "shape": "number" }, + { "name": "licensedUsers", "displayName": "Licensed Users", "shape": "number" } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/estateRestoreSummary.json b/plugins/Redstor/v1/dataStreams/estateRestoreSummary.json new file mode 100644 index 00000000..03d8100b --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/estateRestoreSummary.json @@ -0,0 +1,31 @@ +{ + "name": "estateRestoreSummary", + "displayName": "Estate Restore Summary", + "description": "Current restore operation status by product, for the Partner company and rolled up across its customers", + "tags": ["Restore"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/restores/status/summary", + "getArgs": [{ "key": "companyId", "value": "{{dataSource.entityIdentifier}}" }], + "postRequestScript": "estateRestoreSummary.js" + }, + "matches": "none", + "metadata": [ + { "name": "scope", "displayName": "Scope", "shape": "string" }, + { "name": "productName", "displayName": "Product", "shape": "string", "role": "label" }, + { "name": "succeeded", "displayName": "Succeeded", "shape": "number" }, + { "name": "completedWithWarnings", "displayName": "Completed with Warnings", "shape": "number" }, + { "name": "completedWithErrors", "displayName": "Completed with Errors", "shape": "number" }, + { "name": "failed", "displayName": "Failed", "shape": "number" }, + { + "name": "overallStatus", + "displayName": "Status", + "computed": true, + "valueExpression": "{{ $['failed'] > 0 ? 'error' : ($['completedWithErrors'] > 0 ? 'warning' : 'success') }}", + "shape": "state" + }, + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/partnerSubscriptions.json b/plugins/Redstor/v1/dataStreams/partnerSubscriptions.json new file mode 100644 index 00000000..5b051c19 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/partnerSubscriptions.json @@ -0,0 +1,58 @@ +{ + "name": "partnerSubscriptions", + "displayName": "Partner Subscriptions", + "description": "Product subscriptions for the configured Partner company", + "tags": ["Usage"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/subscriptions", + "getArgs": [{ "key": "companyId", "value": "{{dataSource.entityIdentifier}}" }], + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "maxResults", "value": "100" }, + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "queryArg", "path": "pageToken" } + }, + "pathToData": "results" + }, + "matches": "none", + "metadata": [ + { "name": "productId", "displayName": "Product ID", "shape": "number", "visible": false }, + { "name": "editionId", "displayName": "Edition ID", "shape": "number", "visible": false }, + { + "name": "productName", + "displayName": "Product", + "sourceId": "productId", + "sourceType": "Product", + "objectPropertyPath": "name", + "role": "label" + }, + { + "name": "editionName", + "displayName": "Edition", + "sourceId": "editionId", + "sourceType": "Edition", + "objectPropertyPath": "name" + }, + { "name": "subscriptionId", "displayName": "Subscription ID", "shape": "guid" }, + { "name": "isActive", "displayName": "Active", "shape": "boolean" }, + { + "name": "trialStatus", + "displayName": "Trial Status", + "shape": [ + "state", + { + "map": { + "success": ["Upgraded"], + "unknown": ["NotUsed"], + "warning": ["Trial"], + "error": ["Expired"], + "unmonitored": ["Removed"] + } + } + ] + } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/scripts/estateBackupSummary.js b/plugins/Redstor/v1/dataStreams/scripts/estateBackupSummary.js new file mode 100644 index 00000000..bebdf200 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/estateBackupSummary.js @@ -0,0 +1,7 @@ +// GET /backups/status/summary returns two arrays by product: companySummary +// (the configured Partner company) and customerSummary (rolled up across every +// customer beneath it) — tag each row with its scope so both can still be told +// apart, while a tile that groups by product alone combines the two. +const tag = (rows, scope) => (rows || []).map((row) => ({ scope, ...row })); + +result = [...tag(data?.companySummary, "Partner company"), ...tag(data?.customerSummary, "Customer companies")]; diff --git a/plugins/Redstor/v1/dataStreams/scripts/estateConsumption.js b/plugins/Redstor/v1/dataStreams/scripts/estateConsumption.js new file mode 100644 index 00000000..65769a7c --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/estateConsumption.js @@ -0,0 +1,7 @@ +// GET /consumption/summary returns one companySummary object (the configured +// Partner company) and one customerSummary object (rolled up across every +// customer beneath it), not arrays — tag each as its own row. +result = [ + { scope: "Partner company", ...(data?.companySummary || {}) }, + { scope: "Customer companies", ...(data?.customerSummary || {}) }, +]; diff --git a/plugins/Redstor/v1/dataStreams/scripts/estateRestoreSummary.js b/plugins/Redstor/v1/dataStreams/scripts/estateRestoreSummary.js new file mode 100644 index 00000000..1befd864 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/estateRestoreSummary.js @@ -0,0 +1,7 @@ +// GET /restores/status/summary returns two arrays by product: companySummary +// (the configured Partner company) and customerSummary (rolled up across every +// customer beneath it) — tag each row with its scope so both can still be told +// apart, while a tile that groups by product alone combines the two. +const tag = (rows, scope) => (rows || []).map((row) => ({ scope, ...row })); + +result = [...tag(data?.companySummary, "Partner company"), ...tag(data?.customerSummary, "Customer companies")]; diff --git a/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json index 1d4dd126..3170337e 100644 --- a/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json +++ b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json @@ -1,5 +1,5 @@ { - "name": "Account", + "name": "Account view", "schemaVersion": "1.5", "timeframe": "none", "variables": ["{{variables.[Account]}}"], diff --git a/plugins/Redstor/v1/defaultContent/overview.dash.json b/plugins/Redstor/v1/defaultContent/overview.dash.json index e47312a8..4c1bcdc5 100644 --- a/plugins/Redstor/v1/defaultContent/overview.dash.json +++ b/plugins/Redstor/v1/defaultContent/overview.dash.json @@ -2,14 +2,14 @@ "name": "Overview", "schemaVersion": "1.5", "timeframe": "none", - "variables": ["{{variables.[Company]}}"], + "variables": [], "dashboard": { "_type": "layout/grid", "columns": 4, "version": 1, "contents": [ { - "i": "9795faee-8a6d-46c7-92e8-8d001e6b8983", + "i": "8e30bcb6-7300-4281-be83-23bfdbcf639c", "x": 0, "y": 0, "w": 1, @@ -19,21 +19,18 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Company Accounts", + "title": "Companies", "description": "", "activePluginConfigIds": ["{{configId}}"], "timeframe": "none", - "variables": ["{{variables.[Company]}}"], "dataStream": { - "id": "{{dataStreams.backupAccounts}}", - "name": "backupAccounts", - "pluginConfigId": "{{configId}}", + "id": "datastream-properties", + "name": "properties", "group": { "by": [], "aggregate": [{ "type": "count" }] } }, "scope": { "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" + "workspace": "{{workspaceId}}" }, "visualisation": { "type": "data-stream-scalar", @@ -47,7 +44,7 @@ } }, { - "i": "8b1f0a2c-aa2a-414a-bbb2-b344f58db72a", + "i": "0c0f5b54-780a-4646-b93a-89f93a10b6b7", "x": 1, "y": 0, "w": 1, @@ -61,18 +58,12 @@ "description": "", "activePluginConfigIds": ["{{configId}}"], "timeframe": "none", - "variables": ["{{variables.[Company]}}"], "dataStream": { - "id": "{{dataStreams.companyBackupSummary}}", - "name": "companyBackupSummary", + "id": "{{dataStreams.estateBackupSummary}}", + "name": "estateBackupSummary", "pluginConfigId": "{{configId}}", "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["failed"] }] } }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, "visualisation": { "type": "data-stream-scalar", "config": { @@ -85,7 +76,7 @@ } }, { - "i": "b040da9c-f4ae-4818-aa73-620ecb89b204", + "i": "39274aa9-c5fe-41d9-aeb7-b2106016d6b7", "x": 2, "y": 0, "w": 1, @@ -99,18 +90,12 @@ "description": "", "activePluginConfigIds": ["{{configId}}"], "timeframe": "none", - "variables": ["{{variables.[Company]}}"], "dataStream": { - "id": "{{dataStreams.companyRestoreSummary}}", - "name": "companyRestoreSummary", + "id": "{{dataStreams.estateRestoreSummary}}", + "name": "estateRestoreSummary", "pluginConfigId": "{{configId}}", "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["failed"] }] } }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, "visualisation": { "type": "data-stream-scalar", "config": { @@ -123,7 +108,7 @@ } }, { - "i": "7235ac85-2443-4417-abfa-6e69c9b54ad3", + "i": "a491a8dd-86aa-432e-82d7-443f26fb4db1", "x": 3, "y": 0, "w": 1, @@ -137,18 +122,12 @@ "description": "", "activePluginConfigIds": ["{{configId}}"], "timeframe": "none", - "variables": ["{{variables.[Company]}}"], "dataStream": { - "id": "{{dataStreams.companyConsumption}}", - "name": "companyConsumption", + "id": "{{dataStreams.estateConsumption}}", + "name": "estateConsumption", "pluginConfigId": "{{configId}}", "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["totalDataProtectedInBytes"] }] } }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, "visualisation": { "type": "data-stream-scalar", "config": { @@ -161,7 +140,7 @@ } }, { - "i": "35dd8102-8333-4e6b-8f47-ee6cc6f4365b", + "i": "fbc72ef8-4641-44d4-b815-c60fc5cc3202", "x": 0, "y": 2, "w": 2, @@ -175,23 +154,33 @@ "description": "", "activePluginConfigIds": ["{{configId}}"], "timeframe": "none", - "variables": ["{{variables.[Company]}}"], "dataStream": { - "id": "{{dataStreams.companyBackupSummary}}", - "name": "companyBackupSummary", - "pluginConfigId": "{{configId}}" - }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" + "id": "{{dataStreams.estateBackupSummary}}", + "name": "estateBackupSummary", + "pluginConfigId": "{{configId}}", + "group": { + "by": [["productName", "uniqueValues"]], + "aggregate": [ + { "type": "sum", "names": ["succeeded"] }, + { "type": "sum", "names": ["completedWithWarnings"] }, + { "type": "sum", "names": ["completedWithErrors"] }, + { "type": "sum", "names": ["failed"] }, + { "type": "sum", "names": ["missed"] } + ] + } }, "visualisation": { "type": "data-stream-bar-chart", "config": { "data-stream-bar-chart": { - "xAxisData": "productName", - "yAxisData": ["succeeded", "completedWithWarnings", "completedWithErrors", "failed", "missed"], + "xAxisData": "productName_uniqueValues", + "yAxisData": [ + "succeeded_sum", + "completedWithWarnings_sum", + "completedWithErrors_sum", + "failed_sum", + "missed_sum" + ], "xAxisGroup": "none", "horizontalLayout": "vertical", "grouping": false, @@ -208,7 +197,7 @@ } }, { - "i": "74364c93-31bd-4e81-8c00-87b79b183990", + "i": "e9def5d0-5073-45f9-8f10-bc8b4aae5695", "x": 2, "y": 2, "w": 2, @@ -222,23 +211,31 @@ "description": "", "activePluginConfigIds": ["{{configId}}"], "timeframe": "none", - "variables": ["{{variables.[Company]}}"], "dataStream": { - "id": "{{dataStreams.companyRestoreSummary}}", - "name": "companyRestoreSummary", - "pluginConfigId": "{{configId}}" - }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" + "id": "{{dataStreams.estateRestoreSummary}}", + "name": "estateRestoreSummary", + "pluginConfigId": "{{configId}}", + "group": { + "by": [["productName", "uniqueValues"]], + "aggregate": [ + { "type": "sum", "names": ["succeeded"] }, + { "type": "sum", "names": ["completedWithWarnings"] }, + { "type": "sum", "names": ["completedWithErrors"] }, + { "type": "sum", "names": ["failed"] } + ] + } }, "visualisation": { "type": "data-stream-bar-chart", "config": { "data-stream-bar-chart": { - "xAxisData": "productName", - "yAxisData": ["succeeded", "completedWithWarnings", "completedWithErrors", "failed"], + "xAxisData": "productName_uniqueValues", + "yAxisData": [ + "succeeded_sum", + "completedWithWarnings_sum", + "completedWithErrors_sum", + "failed_sum" + ], "xAxisGroup": "none", "horizontalLayout": "vertical", "grouping": false, @@ -255,7 +252,7 @@ } }, { - "i": "814ace7e-f1e7-47b1-89e4-e1312e1e79ab", + "i": "405ef4a0-1e84-4c8e-b9e6-3a823d83183f", "x": 0, "y": 6, "w": 2, @@ -269,32 +266,24 @@ "description": "", "activePluginConfigIds": ["{{configId}}"], "timeframe": "none", - "variables": ["{{variables.[Company]}}"], "dataStream": { - "id": "{{dataStreams.companyConsumption}}", - "name": "companyConsumption", + "id": "{{dataStreams.estateConsumption}}", + "name": "estateConsumption", "pluginConfigId": "{{configId}}" }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, "visualisation": { "type": "data-stream-table", "config": { "data-stream-table": { "transpose": false, "columnOrder": [ - "productName", + "scope", "totalDataProtectedInBytes", "activeSeats", "inactiveSeats", "sharedSeats", "activeWorkloads", - "licensedUsers", - "isTrial", - "sku" + "licensedUsers" ], "hiddenColumns": ["totalDataProtectedInBytesExternal", "fupOverageInBytes"] } @@ -303,7 +292,7 @@ } }, { - "i": "54c3dab5-4717-4d96-9b89-5a301fd7085e", + "i": "c9646d0f-24e0-4ee3-b035-fd2a0654793f", "x": 2, "y": 6, "w": 2, @@ -317,17 +306,11 @@ "description": "", "activePluginConfigIds": ["{{configId}}"], "timeframe": "none", - "variables": ["{{variables.[Company]}}"], "dataStream": { - "id": "{{dataStreams.companySubscriptions}}", - "name": "companySubscriptions", + "id": "{{dataStreams.partnerSubscriptions}}", + "name": "partnerSubscriptions", "pluginConfigId": "{{configId}}" }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, "visualisation": { "type": "data-stream-table", "config": { @@ -338,44 +321,6 @@ } } } - }, - { - "i": "3d81b3b7-34d2-4ad0-ad7f-bb30b18bb7ff", - "x": 0, - "y": 10, - "w": 4, - "h": 4, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Company Accounts", - "description": "", - "activePluginConfigIds": ["{{configId}}"], - "timeframe": "none", - "variables": ["{{variables.[Company]}}"], - "dataStream": { - "id": "{{dataStreams.backupAccounts}}", - "name": "backupAccounts", - "pluginConfigId": "{{configId}}" - }, - "scope": { - "scope": "{{scopes.[Companies]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Company]}}" - }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": ["accountName", "serviceName", "productName", "storageRegion", "createOn"], - "hiddenColumns": ["displayName"] - } - } - } - } } ] } diff --git a/plugins/Redstor/v1/docs/README.md b/plugins/Redstor/v1/docs/README.md index d5f37cbb..280369bc 100644 --- a/plugins/Redstor/v1/docs/README.md +++ b/plugins/Redstor/v1/docs/README.md @@ -33,13 +33,20 @@ On save, the plugin authenticates and calls your company's profile; an invalid C **Seats and accounts are not the same number.** Redstor bills per seat, where a seat is one person. A single seat usually holds several backup accounts: a Microsoft 365 seat is an Exchange account plus a OneDrive account, and SharePoint and Teams sites add more on top. A company's account count will therefore be considerably higher than its seat count, so take seat figures from **Company Consumption** rather than by counting accounts. -The out-of-the-box dashboards include a Company **Overview** and a perspective for each **Account**. +The out-of-the-box dashboards are an **Overview** (your whole estate — the Partner company plus every customer beneath it, no selection needed) and an **Account view** perspective for drilling into a single backup account. ## Data streams -Start with the **Company** streams to see backup and restore health across every account in a company at a glance. When one needs investigating, switch to the matching **Account** stream for that account's full run history and error detail. +Start with the **Estate** streams for an at-a-glance view across your whole Partner account, or the **Company** streams to see backup and restore health for one company at a time. When an account needs investigating, switch to the matching **Account** stream for that account's full run history and error detail. -**Company streams — for summarising activity across a company** +**Estate streams — for summarising activity across the whole Partner account (no company selection needed)** + +- **Estate Backup Summary** — backup results by product, split into the Partner company's own totals and a rollup across every customer beneath it. +- **Estate Restore Summary** — restore results by product, split the same way. +- **Estate Consumption** — seat counts, licensed users and data protected, split the same way. +- **Partner Subscriptions** — which products the Partner company itself is subscribed to. Redstor has no equivalent "across all customers" endpoint for subscriptions, so this covers the Partner company only. + +**Company streams — for summarising activity across one company, picked via a scope or dashboard variable** - **Company Backup Summary** — backup results by product for a company, counting succeeded, warnings, errors, failures and missed runs. - **Company Restore Summary** — restore results by product for a company. @@ -47,6 +54,7 @@ Start with the **Company** streams to see backup and restore health across every - **Company Restores** — the current restore status for every account in a company. - **Company Accounts** — every backup account in a company, one row per account, with its product, service, storage region and creation date. - **Company Consumption** — seat counts, licensed users, data protected and fair use overage by product, for a company. +- **Company Subscriptions** — which products a company is subscribed to, and whether each is on trial. **Account streams — for tracking down an individual account's history and errors** @@ -78,6 +86,7 @@ Seats are not indexed as objects, because Redstor reports them as counts rather - **No billable seat total** — Redstor reports active, inactive and shared seats separately but publishes no chargeable figure, so billing has to be worked out from your own agreement and Redstor's fair use rules rather than read directly. - **Restore status codes are not documented** — Redstor publishes no meaning for the numeric restore status values, so they appear as a raw number rather than a health colour. Backup status codes are documented and do show as colours. On a company with no restore activity, the stream lists accounts with no status against them. - **No groups or collections** — Redstor's grouping features are not available through the API and cannot be reported on. -- **Only direct customers are included** — your Partner company and the customers directly beneath it are indexed; deeper reseller-of-reseller chains are not followed. +- **Only direct customers are included** — your Partner company and the customers directly beneath it are indexed; deeper reseller-of-reseller chains are not followed. The Estate streams' customer rollup only reaches as far as this same direct-customer set. +- **Estate Consumption has no per-product breakdown** — unlike Company Consumption, Redstor's consumption summary endpoint returns one total per company, not one row per product. - **Subject to Redstor's fair use throttling** — RedAPI is rate limited and the limits are not published, so very large estates may import slowly. - **Read-only** — the plugin never creates, modifies, or deletes anything in Redstor. From 3837f5a2de8117dc62e355edd16c6dfdaa7c1e63 Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:10:39 +0100 Subject: [PATCH 14/24] Use title case for Redstor Account View dashboard name Co-Authored-By: Claude Sonnet 5.5 --- plugins/Redstor/v1/defaultContent/accountPerspective.dash.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json index 3170337e..93701519 100644 --- a/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json +++ b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json @@ -1,5 +1,5 @@ { - "name": "Account view", + "name": "Account View", "schemaVersion": "1.5", "timeframe": "none", "variables": ["{{variables.[Account]}}"], From 19e6aaf29fdf47b35541390bcb83aae947d40cdb Mon Sep 17 00:00:00 2001 From: Tim Wheeler Date: Tue, 6 Oct 2026 16:58:36 +0100 Subject: [PATCH 15/24] Add Redstor correlation rules and update README wording Add rules relating Accounts to their Company and customer Companies to their parent. Index companyId on Company objects so the rules can match. Co-Authored-By: Claude Sonnet 5.5 --- .../relate-account-to-company.json | 20 +++++++++++++++++++ .../relate-company-to-parent-company.json | 20 +++++++++++++++++++ plugins/Redstor/v1/docs/README.md | 10 +++++----- .../Redstor/v1/indexDefinitions/default.json | 4 ++-- 4 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 plugins/Redstor/v1/correlationRules/relate-account-to-company.json create mode 100644 plugins/Redstor/v1/correlationRules/relate-company-to-parent-company.json diff --git a/plugins/Redstor/v1/correlationRules/relate-account-to-company.json b/plugins/Redstor/v1/correlationRules/relate-account-to-company.json new file mode 100644 index 00000000..8d367321 --- /dev/null +++ b/plugins/Redstor/v1/correlationRules/relate-account-to-company.json @@ -0,0 +1,20 @@ +{ + "displayName": "Company has Account", + "source": { + "types": "Account" + }, + "target": { + "types": "Company" + }, + "conditions": [ + { + "sourceProperty": "companyId", + "operator": "equals", + "targetProperty": "companyId" + } + ], + "labels": { + "forward": "belongs to", + "reverse": "has" + } +} diff --git a/plugins/Redstor/v1/correlationRules/relate-company-to-parent-company.json b/plugins/Redstor/v1/correlationRules/relate-company-to-parent-company.json new file mode 100644 index 00000000..f42308cf --- /dev/null +++ b/plugins/Redstor/v1/correlationRules/relate-company-to-parent-company.json @@ -0,0 +1,20 @@ +{ + "displayName": "Company has customer Company", + "source": { + "types": "Company" + }, + "target": { + "types": "Company" + }, + "conditions": [ + { + "sourceProperty": "parentCompanyId", + "operator": "equals", + "targetProperty": "companyId" + } + ], + "labels": { + "forward": "is a customer of", + "reverse": "has customer" + } +} diff --git a/plugins/Redstor/v1/docs/README.md b/plugins/Redstor/v1/docs/README.md index 280369bc..f945d944 100644 --- a/plugins/Redstor/v1/docs/README.md +++ b/plugins/Redstor/v1/docs/README.md @@ -1,6 +1,6 @@ Monitor your [Redstor](https://www.redstor.com) Partner account in SquaredUp — customer companies, seat and storage consumption, backup and restore health, and product subscriptions — via the [RedAPI](https://www.redstor.com/our-technology/integrations/redapi/). -> ⚠️ RedAPI is only available to **Partner Admin** users on current Redstor pricing plans — Company Admins and legacy-plan accounts cannot use it. This plugin does not cover Redstor's older Storage Platform REST API. +> ⚠️ RedAPI is only available to **Partner Admin** users on current Redstor pricing plans — Company Admins and legacy-plan accounts cannot use it. This data source does not cover Redstor's older Storage Platform REST API. ## Setup @@ -21,9 +21,9 @@ You will need a RedAPI **service account** with its Client ID and private key, p | **Client ID** | Identifies the RedAPI service account used to authenticate. | RedApp → **RedAPI → Service accounts** → your service account's key. | Yes | | **Private key** | The JSON Web Key (JWK) paired with the Client ID; signs the request used to obtain access tokens. | Downloaded when the service account's key was created. | Yes | -On save, the plugin authenticates and calls your company's profile; an invalid Company ID, Client ID, or Private key fails setup with an authentication error. +On save, the data source authenticates and calls your company's profile; an invalid Company ID, Client ID, or Private key fails setup with an authentication error. -## What this plugin monitors +## What this data source contains - **Customer companies** — your Partner company and the customers beneath it. - **Seat usage and billing exposure** — active, inactive and shared seats, licensed users, data protected and fair use overage, broken down by product for each company. @@ -73,7 +73,7 @@ Start with the **Estate** streams for an at-a-glance view across your whole Part | **Service** | `GET /products` | A workload within a product, such as Exchange, OneDrive, SharePoint or Teams within O365. | | **Edition** | `GET /products` | An edition of a product, such as Premium. | -**Relationships:** each Account belongs to its parent Company. +**Relationships:** each Account belongs to its parent Company, and each customer Company belongs to its parent Company. Seats are not indexed as objects, because Redstor reports them as counts rather than as individually addressable records. They appear in **Company Consumption**. @@ -89,4 +89,4 @@ Seats are not indexed as objects, because Redstor reports them as counts rather - **Only direct customers are included** — your Partner company and the customers directly beneath it are indexed; deeper reseller-of-reseller chains are not followed. The Estate streams' customer rollup only reaches as far as this same direct-customer set. - **Estate Consumption has no per-product breakdown** — unlike Company Consumption, Redstor's consumption summary endpoint returns one total per company, not one row per product. - **Subject to Redstor's fair use throttling** — RedAPI is rate limited and the limits are not published, so very large estates may import slowly. -- **Read-only** — the plugin never creates, modifies, or deletes anything in Redstor. +- **Read-only** — the data source never creates, modifies, or deletes anything in Redstor. diff --git a/plugins/Redstor/v1/indexDefinitions/default.json b/plugins/Redstor/v1/indexDefinitions/default.json index 46736c15..0a74bff2 100644 --- a/plugins/Redstor/v1/indexDefinitions/default.json +++ b/plugins/Redstor/v1/indexDefinitions/default.json @@ -8,7 +8,7 @@ "id": "companyId", "name": "companyName", "type": { "value": "Company" }, - "properties": ["type", "isEnabled", "parentCompanyId"] + "properties": ["companyId", "type", "isEnabled", "parentCompanyId"] } }, { @@ -19,7 +19,7 @@ "id": "companyId", "name": "companyName", "type": { "value": "Company" }, - "properties": ["type", "isEnabled", "parentCompanyId"] + "properties": ["companyId", "type", "isEnabled", "parentCompanyId"] } }, { From 174cb23cb20ef6b9d1f98b22786879f61a1af056 Mon Sep 17 00:00:00 2001 From: Tim Wheeler Date: Tue, 6 Oct 2026 17:04:40 +0100 Subject: [PATCH 16/24] Update Redstor README Co-Authored-By: Claude Sonnet 5.5 --- plugins/Redstor/v1/docs/README.md | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/plugins/Redstor/v1/docs/README.md b/plugins/Redstor/v1/docs/README.md index f945d944..62bfcc7e 100644 --- a/plugins/Redstor/v1/docs/README.md +++ b/plugins/Redstor/v1/docs/README.md @@ -1,11 +1,11 @@ -Monitor your [Redstor](https://www.redstor.com) Partner account in SquaredUp — customer companies, seat and storage consumption, backup and restore health, and product subscriptions — via the [RedAPI](https://www.redstor.com/our-technology/integrations/redapi/). - -> ⚠️ RedAPI is only available to **Partner Admin** users on current Redstor pricing plans — Company Admins and legacy-plan accounts cannot use it. This data source does not cover Redstor's older Storage Platform REST API. +Monitor your [Redstor](https://www.redstor.com) Partner account in SquaredUp — customer companies, seat and storage consumption, backup and restore health, and product subscriptions all via the [RedAPI](https://www.redstor.com/our-technology/integrations/redapi/). ## Setup You will need a RedAPI **service account** with its Client ID and private key, plus your Partner company's ID. +> ⚠️ RedAPI is only available to **Partner Admin** users on current Redstor pricing plans — Company Admins and legacy-plan accounts cannot use it. This data source does not cover Redstor's older Storage Platform REST API. + 1. Sign in to [RedApp](https://redapp.redstor.com) as a **Partner Admin**. 2. Note your Partner company's ID — find it under **Company Settings** — and paste it into the **Company ID** field. 3. Go to **RedAPI → Service accounts** and click to add a new service account. Give it a descriptive name and assign it access to your Partner company (and the customers you want visibility into). @@ -31,14 +31,8 @@ On save, the data source authenticates and calls your company's profile; an inva - **Backup accounts** — every protected mailbox, drive, site and machine, with the product and service it belongs to. - **Subscriptions** — which products each company is subscribed to, and whether each is on trial. -**Seats and accounts are not the same number.** Redstor bills per seat, where a seat is one person. A single seat usually holds several backup accounts: a Microsoft 365 seat is an Exchange account plus a OneDrive account, and SharePoint and Teams sites add more on top. A company's account count will therefore be considerably higher than its seat count, so take seat figures from **Company Consumption** rather than by counting accounts. - -The out-of-the-box dashboards are an **Overview** (your whole estate — the Partner company plus every customer beneath it, no selection needed) and an **Account view** perspective for drilling into a single backup account. - ## Data streams -Start with the **Estate** streams for an at-a-glance view across your whole Partner account, or the **Company** streams to see backup and restore health for one company at a time. When an account needs investigating, switch to the matching **Account** stream for that account's full run history and error detail. - **Estate streams — for summarising activity across the whole Partner account (no company selection needed)** - **Estate Backup Summary** — backup results by product, split into the Partner company's own totals and a rollup across every customer beneath it. @@ -73,20 +67,10 @@ Start with the **Estate** streams for an at-a-glance view across your whole Part | **Service** | `GET /products` | A workload within a product, such as Exchange, OneDrive, SharePoint or Teams within O365. | | **Edition** | `GET /products` | An edition of a product, such as Premium. | -**Relationships:** each Account belongs to its parent Company, and each customer Company belongs to its parent Company. - -Seats are not indexed as objects, because Redstor reports them as counts rather than as individually addressable records. They appear in **Company Consumption**. - -## Known limitations +## Notes -- **No historical data** — every stream reports the current state, as RedAPI exposes no time ranges, so tiles have no timeframe selection. Consumption is the one partial exception: it can report a single past date, but not a range or a trend. -- **Backup history can be large** — Company Backups returns every run in Redstor's rolling seven day window for every account in the company, which on a large customer can be more data than a single tile can return. Account Backups covers the same window for one account at a time, so it stays small regardless of company size. - **Accounts that have never run a backup have no error detail** — Redstor only keeps error messages for accounts that have backup history. The Account Backup Errors and Account Restore Errors streams say so rather than returning rows. In testing this applied to roughly a third of a company's accounts. -- **Machine products report workloads, not seats** — Machines and Azure Virtual Machines return zero for every seat count and are measured by workload count instead, so seat-based tiles will look empty for those products. -- **No billable seat total** — Redstor reports active, inactive and shared seats separately but publishes no chargeable figure, so billing has to be worked out from your own agreement and Redstor's fair use rules rather than read directly. -- **Restore status codes are not documented** — Redstor publishes no meaning for the numeric restore status values, so they appear as a raw number rather than a health colour. Backup status codes are documented and do show as colours. On a company with no restore activity, the stream lists accounts with no status against them. - **No groups or collections** — Redstor's grouping features are not available through the API and cannot be reported on. -- **Only direct customers are included** — your Partner company and the customers directly beneath it are indexed; deeper reseller-of-reseller chains are not followed. The Estate streams' customer rollup only reaches as far as this same direct-customer set. -- **Estate Consumption has no per-product breakdown** — unlike Company Consumption, Redstor's consumption summary endpoint returns one total per company, not one row per product. +- **Only direct customers are included** — your Partner company and the customers directly beneath it are indexed; deeper reseller-of-reseller chains are not followed. Raise an issue if you require this extra level of detail. - **Subject to Redstor's fair use throttling** — RedAPI is rate limited and the limits are not published, so very large estates may import slowly. - **Read-only** — the data source never creates, modifies, or deletes anything in Redstor. From 029c7176862fb5bbc5fb91a23edfe38cd24fb293 Mon Sep 17 00:00:00 2001 From: Tim Wheeler Date: Wed, 7 Oct 2026 16:09:36 +0100 Subject: [PATCH 17/24] Sequence Redstor product index steps with dependsOn Chain products -> productServices -> productEditions so the three /products calls no longer run in parallel, in case RedAPI throttling caused the intermittent 401s seen during indexing. Co-Authored-By: Claude Sonnet 5.5 --- plugins/Redstor/v1/indexDefinitions/default.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/Redstor/v1/indexDefinitions/default.json b/plugins/Redstor/v1/indexDefinitions/default.json index 0a74bff2..9c8cef4c 100644 --- a/plugins/Redstor/v1/indexDefinitions/default.json +++ b/plugins/Redstor/v1/indexDefinitions/default.json @@ -41,7 +41,8 @@ "name": "serviceName", "type": { "value": "Service" }, "properties": ["productId", "productName"] - } + }, + "dependsOn": ["products"] }, { "name": "productEditions", @@ -52,7 +53,8 @@ "name": "editionName", "type": { "value": "Edition" }, "properties": ["hasTrial"] - } + }, + "dependsOn": ["productServices"] }, { "name": "backupAccounts", From f0b4937e70242360bda88da0d47e7189c7d1e929 Mon Sep 17 00:00:00 2001 From: Tim Wheeler Date: Wed, 7 Oct 2026 17:48:02 +0100 Subject: [PATCH 18/24] Convert Redstor dashboards to SmartViz and add Company View Replace classic visualisations on the Overview and Account View dashboards with vizSpec tiles (scalar, bar and richTable), update their layouts, and add a Company View dashboard scoped by the Company variable. Co-Authored-By: Claude Sonnet 5.5 --- .../accountPerspective.dash.json | 309 ++++++++++-- .../companyPerspective.dash.json | 457 ++++++++++++++++++ .../Redstor/v1/defaultContent/manifest.json | 1 + .../v1/defaultContent/overview.dash.json | 339 +++++++------ 4 files changed, 912 insertions(+), 194 deletions(-) create mode 100644 plugins/Redstor/v1/defaultContent/companyPerspective.dash.json diff --git a/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json index 93701519..2b52cee8 100644 --- a/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json +++ b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json @@ -10,7 +10,7 @@ "contents": [ { "i": "10564f29-ffb4-4591-b33c-3232a7cf96f8", - "x": 0, + "x": 1, "y": 0, "w": 1, "h": 2, @@ -35,12 +35,21 @@ "workspace": "{{workspaceId}}", "variable": "{{variables.[Account]}}" }, - "visualisation": { - "type": "data-stream-scalar", - "config": { - "data-stream-scalar": { - "value": "count_sum", - "comparisonColumn": "none" + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count_sum", + "title": "Backup Errors", + "type": "quantitative" + } + ] + }, + "style": { + "scalarLabelPosition": "none" } } } @@ -48,7 +57,7 @@ }, { "i": "fe0264d0-3511-48d1-9ce1-a860df56c8a4", - "x": 1, + "x": 2, "y": 0, "w": 1, "h": 2, @@ -73,12 +82,21 @@ "workspace": "{{workspaceId}}", "variable": "{{variables.[Account]}}" }, - "visualisation": { - "type": "data-stream-scalar", - "config": { - "data-stream-scalar": { - "value": "count_sum", - "comparisonColumn": "none" + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count_sum", + "title": "Restore Errors", + "type": "quantitative" + } + ] + }, + "style": { + "scalarLabelPosition": "none" } } } @@ -87,9 +105,9 @@ { "i": "5900ae96-3e88-4e9f-8d0c-f8a90d1da438", "x": 0, - "y": 2, - "w": 2, - "h": 4, + "y": 0, + "w": 1, + "h": 2, "moved": false, "static": false, "z": 0, @@ -109,20 +127,57 @@ "workspace": "{{workspaceId}}", "variable": "{{variables.[Account]}}" }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { "transpose": true } + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "name", + "title": "Name", + "renderer": { + "type": "text" + } + }, + { + "field": "serviceName", + "title": "Service", + "renderer": { + "type": "text" + } + }, + { + "field": "productName", + "title": "Product", + "renderer": { + "type": "text" + } + }, + { + "field": "storageRegion", + "title": "Storage Region", + "renderer": { + "type": "text" + } + } + ] + }, + "style": { + "richTableTranspose": true, + "richTableFontSize": 14, + "richTableCellPadding": 8 + } } } } }, { "i": "9b8aa0c2-f983-49a2-bffb-866e73dfd37d", - "x": 2, + "x": 0, "y": 2, "w": 2, - "h": 4, + "h": 3, "moved": false, "static": false, "z": 0, @@ -144,12 +199,54 @@ "workspace": "{{workspaceId}}", "variable": "{{variables.[Account]}}" }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": ["timestamp", "message", "category", "type", "count"] + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "timestamp", + "title": "Time", + "renderer": { + "type": "date" + } + }, + { + "field": "message", + "title": "Message", + "renderer": { + "type": "text" + } + }, + { + "field": "category", + "title": "Category", + "renderer": { + "type": "badge", + "defaultColor": "#c20f3c" + } + }, + { + "field": "type", + "title": "Type", + "renderer": { + "type": "text" + } + }, + { + "field": "count", + "title": "Count", + "renderer": { + "type": "number" + }, + "align": "right" + } + ] + }, + "style": { + "richTableFontSize": 14, + "richTableCellPadding": 8 } } } @@ -157,10 +254,10 @@ }, { "i": "842007ab-6933-49b0-8a6c-cca0473ef35c", - "x": 0, - "y": 6, - "w": 4, - "h": 4, + "x": 2, + "y": 2, + "w": 2, + "h": 3, "moved": false, "static": false, "z": 0, @@ -182,12 +279,58 @@ "workspace": "{{workspaceId}}", "variable": "{{variables.[Account]}}" }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": ["timestamp", "message", "category", "type", "count"] + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "timestamp", + "title": "Time", + "renderer": { + "type": "date" + } + }, + { + "field": "message", + "title": "Message", + "renderer": { + "type": "text" + } + }, + { + "field": "category", + "title": "Category", + "renderer": { + "type": "badge", + "defaultColor": "#c20f3c" + } + }, + { + "field": "type", + "title": "Type", + "renderer": { + "type": "text" + } + }, + { + "field": "count", + "title": "Count", + "renderer": { + "type": "number" + }, + "align": "right" + } + ] + }, + "style": { + "richTableSort": { + "field": "timestamp", + "direction": "desc" + }, + "richTableFontSize": 14, + "richTableCellPadding": 8 } } } @@ -196,7 +339,7 @@ { "i": "2086c180-4d1e-48eb-ace2-6b38858b1e4b", "x": 0, - "y": 10, + "y": 5, "w": 2, "h": 4, "moved": false, @@ -220,12 +363,50 @@ "workspace": "{{workspaceId}}", "variable": "{{variables.[Account]}}" }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": ["timestamp", "overallStatus", "message", "errorCount"] + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "timestamp", + "title": "Time", + "renderer": { + "type": "date" + } + }, + { + "field": "overallStatus", + "title": "Status", + "renderer": { + "type": "status" + } + }, + { + "field": "message", + "title": "Message", + "renderer": { + "type": "text" + } + }, + { + "field": "errorCount", + "title": "Error Count", + "renderer": { + "type": "number" + }, + "align": "right" + } + ] + }, + "style": { + "richTableSort": { + "field": "timestamp", + "direction": "desc" + }, + "richTableFontSize": 14, + "richTableCellPadding": 8 } } } @@ -234,7 +415,7 @@ { "i": "388195c4-3020-4e54-a3a2-2d5dda9e913d", "x": 2, - "y": 10, + "y": 5, "w": 2, "h": 4, "moved": false, @@ -258,12 +439,34 @@ "workspace": "{{workspaceId}}", "variable": "{{variables.[Account]}}" }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": ["timestamp", "status"] + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "timestamp", + "title": "Time", + "renderer": { + "type": "date" + } + }, + { + "field": "status", + "title": "Status Code", + "renderer": { + "type": "number" + }, + "align": "right" + } + ] + }, + "style": { + "richTableSort": { + "field": "timestamp", + "direction": "desc" + } } } } diff --git a/plugins/Redstor/v1/defaultContent/companyPerspective.dash.json b/plugins/Redstor/v1/defaultContent/companyPerspective.dash.json new file mode 100644 index 00000000..511001a3 --- /dev/null +++ b/plugins/Redstor/v1/defaultContent/companyPerspective.dash.json @@ -0,0 +1,457 @@ +{ + "name": "Company View", + "schemaVersion": "1.5", + "timeframe": "none", + "variables": [ + "{{variables.[Company]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "0b065314-8e61-47f1-930c-361061f5e01e", + "x": 0, + "y": 0, + "w": 4, + "h": 2, + "z": 0, + "moved": false, + "static": false, + "config": { + "_type": "tile/data-stream", + "title": "Company Properties", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "variables": [ + "{{variables.[Company]}}" + ], + "dataStream": { + "id": "{{dataStreams.properties}}", + "name": "properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "name", + "title": "Name", + "renderer": { + "type": "text" + } + }, + { + "field": "type", + "title": "Type", + "renderer": { + "type": "text" + } + }, + { + "field": "isEnabled", + "title": "Enabled", + "renderer": { + "type": "boolean" + } + }, + { + "field": "parentCompanyId", + "title": "Parent Company ID", + "renderer": { + "type": "text" + } + }, + { + "field": "companyId", + "title": "Company ID", + "renderer": { + "type": "text" + } + } + ] + }, + "style": { + "richTableTranspose": true, + "richTableFontSize": 14, + "richTableCellPadding": 8 + } + } + } + } + }, + { + "i": "f4d5e989-8ec9-47bf-8215-70ad2da97f71", + "x": 0, + "y": 2, + "w": 2, + "h": 4, + "z": 0, + "moved": false, + "static": false, + "config": { + "_type": "tile/data-stream", + "title": "Backup Status", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "variables": [ + "{{variables.[Company]}}" + ], + "dataStream": { + "id": "{{dataStreams.companyBackupSummary}}", + "name": "companyBackupSummary", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "productName", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "sum", + "names": [ + "succeeded" + ] + }, + { + "type": "sum", + "names": [ + "completedWithWarnings" + ] + }, + { + "type": "sum", + "names": [ + "completedWithErrors" + ] + }, + { + "type": "sum", + "names": [ + "failed" + ] + }, + { + "type": "sum", + "names": [ + "missed" + ] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "productName_uniqueValues", + "type": "nominal", + "title": "Product" + } + }, + "style": { + "legendPosition": "bottom", + "cartesianStack": "normalize", + "colors": [ + "#2BB660", + "#f59e0b", + "#c85f14", + "#c01616", + "#94a3b8" + ] + } + } + } + } + }, + { + "i": "a586bc25-bdf2-4c3f-85ce-5817ecd5b9cf", + "x": 2, + "y": 2, + "w": 2, + "h": 4, + "z": 0, + "moved": false, + "static": false, + "config": { + "_type": "tile/data-stream", + "title": "Restore Status", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "variables": [ + "{{variables.[Company]}}" + ], + "dataStream": { + "id": "{{dataStreams.companyRestoreSummary}}", + "name": "companyRestoreSummary", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "productName", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "sum", + "names": [ + "succeeded" + ] + }, + { + "type": "sum", + "names": [ + "completedWithWarnings" + ] + }, + { + "type": "sum", + "names": [ + "completedWithErrors" + ] + }, + { + "type": "sum", + "names": [ + "failed" + ] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "productName_uniqueValues", + "type": "nominal", + "title": "Product" + } + }, + "style": { + "legendPosition": "bottom", + "cartesianStack": "normalize", + "colors": [ + "#22c55e", + "#f59e0b", + "#f97316", + "#ef4444" + ] + } + } + } + } + }, + { + "i": "29eac499-f8ae-4a9b-bc03-a8865d916260", + "x": 0, + "y": 6, + "w": 2, + "h": 4, + "z": 0, + "moved": false, + "static": false, + "config": { + "_type": "tile/data-stream", + "title": "Consumption", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "variables": [ + "{{variables.[Company]}}" + ], + "dataStream": { + "id": "{{dataStreams.companyConsumption}}", + "name": "companyConsumption", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "productName", + "title": "Product", + "renderer": { + "type": "text" + } + }, + { + "field": "totalDataProtectedInBytes", + "title": "Data Protected", + "renderer": { + "type": "text" + } + }, + { + "field": "activeSeats", + "title": "Active Seats", + "renderer": { + "type": "number" + }, + "align": "right" + }, + { + "field": "inactiveSeats", + "title": "Inactive Seats", + "renderer": { + "type": "number" + }, + "align": "right" + }, + { + "field": "sharedSeats", + "title": "Shared Seats", + "renderer": { + "type": "number" + }, + "align": "right" + }, + { + "field": "activeWorkloads", + "title": "Active Workloads", + "renderer": { + "type": "number" + }, + "align": "right" + }, + { + "field": "licensedUsers", + "title": "Licensed Users", + "renderer": { + "type": "number" + }, + "align": "right" + } + ] + }, + "style": {} + } + } + } + }, + { + "i": "df32c159-5e72-4f89-880f-02fd8006d3a0", + "x": 2, + "y": 6, + "w": 2, + "h": 4, + "z": 0, + "moved": false, + "static": false, + "config": { + "_type": "tile/data-stream", + "title": "Subscriptions", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "variables": [ + "{{variables.[Company]}}" + ], + "dataStream": { + "id": "{{dataStreams.companySubscriptions}}", + "name": "companySubscriptions", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Company]}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "productName", + "title": "Product", + "renderer": { + "type": "text" + } + }, + { + "field": "editionName", + "title": "Edition", + "renderer": { + "type": "text" + } + }, + { + "field": "trialStatus", + "title": "Trial Status", + "renderer": { + "type": "status" + } + }, + { + "field": "isActive", + "title": "Active", + "renderer": { + "type": "boolean" + } + }, + { + "field": "subscriptionId", + "title": "Subscription ID", + "renderer": { + "type": "text" + } + } + ] + }, + "style": {} + } + } + } + } + ] + } +} diff --git a/plugins/Redstor/v1/defaultContent/manifest.json b/plugins/Redstor/v1/defaultContent/manifest.json index 85c531a0..f0077666 100644 --- a/plugins/Redstor/v1/defaultContent/manifest.json +++ b/plugins/Redstor/v1/defaultContent/manifest.json @@ -1,6 +1,7 @@ { "items": [ { "name": "overview", "type": "dashboard" }, + { "name": "companyPerspective", "type": "dashboard" }, { "name": "accountPerspective", "type": "dashboard" } ] } diff --git a/plugins/Redstor/v1/defaultContent/overview.dash.json b/plugins/Redstor/v1/defaultContent/overview.dash.json index 4c1bcdc5..f2b8f272 100644 --- a/plugins/Redstor/v1/defaultContent/overview.dash.json +++ b/plugins/Redstor/v1/defaultContent/overview.dash.json @@ -32,76 +32,24 @@ "scope": "{{scopes.[Companies]}}", "workspace": "{{workspaceId}}" }, - "visualisation": { - "type": "data-stream-scalar", - "config": { - "data-stream-scalar": { - "value": "count", - "comparisonColumn": "none" - } - } - } - } - }, - { - "i": "0c0f5b54-780a-4646-b93a-89f93a10b6b7", - "x": 1, - "y": 0, - "w": 1, - "h": 2, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Backup Failures", - "description": "", - "activePluginConfigIds": ["{{configId}}"], - "timeframe": "none", - "dataStream": { - "id": "{{dataStreams.estateBackupSummary}}", - "name": "estateBackupSummary", - "pluginConfigId": "{{configId}}", - "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["failed"] }] } - }, - "visualisation": { - "type": "data-stream-scalar", - "config": { - "data-stream-scalar": { - "value": "failed_sum", - "comparisonColumn": "none" - } - } - } - } - }, - { - "i": "39274aa9-c5fe-41d9-aeb7-b2106016d6b7", - "x": 2, - "y": 0, - "w": 1, - "h": 2, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Restore Failures", - "description": "", - "activePluginConfigIds": ["{{configId}}"], - "timeframe": "none", - "dataStream": { - "id": "{{dataStreams.estateRestoreSummary}}", - "name": "estateRestoreSummary", - "pluginConfigId": "{{configId}}", - "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["failed"] }] } - }, - "visualisation": { - "type": "data-stream-scalar", - "config": { - "data-stream-scalar": { - "value": "failed_sum", - "comparisonColumn": "none" + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "title": "Companies", + "field": "count", + "type": "quantitative" + } + ] + }, + "style": { + "scalarLabelPosition": "none", + "scalarValueColor": "#ef4444", + "scalarComparisonPositiveColor": "#ef4444", + "scalarComparisonNegativeColor": "#22c55e" } } } @@ -109,7 +57,7 @@ }, { "i": "a491a8dd-86aa-432e-82d7-443f26fb4db1", - "x": 3, + "x": 1, "y": 0, "w": 1, "h": 2, @@ -128,12 +76,21 @@ "pluginConfigId": "{{configId}}", "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["totalDataProtectedInBytes"] }] } }, - "visualisation": { - "type": "data-stream-scalar", - "config": { - "data-stream-scalar": { - "value": "totalDataProtectedInBytes_sum", - "comparisonColumn": "none" + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "totalDataProtectedInBytes_sum", + "type": "quantitative", + "title": "Data Protected" + } + ] + }, + "style": { + "scalarLabelPosition": "none" } } } @@ -169,28 +126,31 @@ ] } }, - "visualisation": { - "type": "data-stream-bar-chart", - "config": { - "data-stream-bar-chart": { - "xAxisData": "productName_uniqueValues", - "yAxisData": [ - "succeeded_sum", - "completedWithWarnings_sum", - "completedWithErrors_sum", - "failed_sum", - "missed_sum" - ], - "xAxisGroup": "none", - "horizontalLayout": "vertical", - "grouping": false, - "displayMode": "percentage", - "showLegend": true, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "title": "Product", + "type": "nominal", + "field": "productName_uniqueValues" + } + }, + "style": { "legendPosition": "bottom", - "showGrid": true, - "showValue": false, - "showTotals": false, - "range": { "type": "auto" } + "colors": [ + "#2BB660", + "#f59e0b", + "#c85f14", + "#c01616", + "#94a3b8" + ], + "cartesianStack": "normalize", + "cartesianXAxisLabels": true, + "cartesianYAxisLabels": true, + "cartesianYAxisScale": "linear", + "cartesianY2AxisScale": "linear" } } } @@ -225,27 +185,26 @@ ] } }, - "visualisation": { - "type": "data-stream-bar-chart", - "config": { - "data-stream-bar-chart": { - "xAxisData": "productName_uniqueValues", - "yAxisData": [ - "succeeded_sum", - "completedWithWarnings_sum", - "completedWithErrors_sum", - "failed_sum" + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "productName_uniqueValues", + "type": "nominal", + "title": "Product" + } + }, + "style": { + "cartesianStack": "normalize", + "colors": [ + "#22c55e", + "#f59e0b", + "#f97316", + "#ef4444" ], - "xAxisGroup": "none", - "horizontalLayout": "vertical", - "grouping": false, - "displayMode": "percentage", - "showLegend": true, - "legendPosition": "bottom", - "showGrid": true, - "showValue": false, - "showTotals": false, - "range": { "type": "auto" } + "legendPosition": "bottom" } } } @@ -271,22 +230,69 @@ "name": "estateConsumption", "pluginConfigId": "{{configId}}" }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": [ - "scope", - "totalDataProtectedInBytes", - "activeSeats", - "inactiveSeats", - "sharedSeats", - "activeWorkloads", - "licensedUsers" - ], - "hiddenColumns": ["totalDataProtectedInBytesExternal", "fupOverageInBytes"] - } + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "scope", + "title": "Scope", + "renderer": { + "type": "text" + } + }, + { + "field": "totalDataProtectedInBytes", + "title": "Data Protected", + "renderer": { + "type": "text" + } + }, + { + "field": "activeSeats", + "title": "Active Seats", + "renderer": { + "type": "number" + }, + "align": "right" + }, + { + "field": "inactiveSeats", + "title": "Inactive Seats", + "renderer": { + "type": "number" + }, + "align": "right" + }, + { + "field": "sharedSeats", + "title": "Shared Seats", + "renderer": { + "type": "number" + }, + "align": "right" + }, + { + "field": "activeWorkloads", + "title": "Active Workloads", + "renderer": { + "type": "number" + }, + "align": "right" + }, + { + "field": "licensedUsers", + "title": "Licensed Users", + "renderer": { + "type": "number" + }, + "align": "right" + } + ] + }, + "style": {} } } } @@ -311,16 +317,67 @@ "name": "partnerSubscriptions", "pluginConfigId": "{{configId}}" }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": ["productName", "editionName", "trialStatus", "isActive", "subscriptionId"] - } + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "productName", + "title": "Product", + "renderer": { + "type": "text" + } + }, + { + "field": "editionName", + "title": "Edition", + "renderer": { + "type": "text" + } + }, + { + "field": "trialStatus", + "title": "Trial Status", + "renderer": { + "type": "status" + } + }, + { + "field": "isActive", + "title": "Active", + "renderer": { + "type": "boolean" + } + }, + { + "field": "subscriptionId", + "title": "Subscription ID", + "renderer": { + "type": "text" + } + } + ] + }, + "style": {} } } } + }, + { + "i": "bafb42c9-0350-4e60-a1de-00752e8f99db", + "x": 0, + "y": 10, + "w": 4, + "h": 2, + "z": 0, + "moved": false, + "static": false, + "config": { + "title": "", + "description": "" + } } ] } From 7f03fc017dec17bb74cdb3c02f2b9f4cad514989 Mon Sep 17 00:00:00 2001 From: Tim Wheeler Date: Wed, 7 Oct 2026 17:55:31 +0100 Subject: [PATCH 19/24] Update Redstor Company View and Overview dashboards Use the revised Company View layout and drop the red styling from the Overview Companies scalar. Co-Authored-By: Claude Sonnet 5.5 --- .../companyPerspective.dash.json | 18 ++---------------- .../v1/defaultContent/overview.dash.json | 5 +---- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/plugins/Redstor/v1/defaultContent/companyPerspective.dash.json b/plugins/Redstor/v1/defaultContent/companyPerspective.dash.json index 511001a3..381809ec 100644 --- a/plugins/Redstor/v1/defaultContent/companyPerspective.dash.json +++ b/plugins/Redstor/v1/defaultContent/companyPerspective.dash.json @@ -14,7 +14,7 @@ "i": "0b065314-8e61-47f1-930c-361061f5e01e", "x": 0, "y": 0, - "w": 4, + "w": 2, "h": 2, "z": 0, "moved": false, @@ -31,7 +31,7 @@ "{{variables.[Company]}}" ], "dataStream": { - "id": "{{dataStreams.properties}}", + "id": "datastream-properties", "name": "properties", "pluginConfigId": "{{configId}}" }, @@ -66,20 +66,6 @@ "renderer": { "type": "boolean" } - }, - { - "field": "parentCompanyId", - "title": "Parent Company ID", - "renderer": { - "type": "text" - } - }, - { - "field": "companyId", - "title": "Company ID", - "renderer": { - "type": "text" - } } ] }, diff --git a/plugins/Redstor/v1/defaultContent/overview.dash.json b/plugins/Redstor/v1/defaultContent/overview.dash.json index f2b8f272..f8952634 100644 --- a/plugins/Redstor/v1/defaultContent/overview.dash.json +++ b/plugins/Redstor/v1/defaultContent/overview.dash.json @@ -46,10 +46,7 @@ ] }, "style": { - "scalarLabelPosition": "none", - "scalarValueColor": "#ef4444", - "scalarComparisonPositiveColor": "#ef4444", - "scalarComparisonNegativeColor": "#22c55e" + "scalarLabelPosition": "none" } } } From 38dff3214adb6e4fa266b8e6b753f0bb72e057fc Mon Sep 17 00:00:00 2001 From: Tim Wheeler Date: Thu, 8 Oct 2026 11:00:19 +0100 Subject: [PATCH 20/24] Address CodeRabbit review on Redstor plugin - Mask the private key field as a password input - Trim ui.json help text and shorten the metadata description - Remove em-dashes and second-person wording from the README - Map restore status codes to labelled states in the stream JSON - Remove the empty tile from the overview dashboard - Scope the CODEOWNERS rule to the whole Redstor directory Co-Authored-By: Claude Sonnet 5.5 --- .github/CODEOWNERS | 2 +- .../v1/dataStreams/accountRestores.json | 16 +++- .../v1/dataStreams/companyRestores.json | 16 +++- .../accountPerspective.dash.json | 7 +- .../v1/defaultContent/overview.dash.json | 14 ---- plugins/Redstor/v1/docs/README.md | 76 +++++++++---------- plugins/Redstor/v1/metadata.json | 2 +- plugins/Redstor/v1/ui.json | 8 +- 8 files changed, 76 insertions(+), 65 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4dd5cd37..707940f8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -18,7 +18,7 @@ plugins/MetOffice/* @blackgrouse plugins/Phare/* @vinbab plugins/Postcoder/* @richbenwell plugins/RDAP/* @richbenwell -plugins/Redstor/* @TimWheeler-SQUP +plugins/Redstor/ @TimWheeler-SQUP plugins/RSS/* @clarkd plugins/Rootly/* @noorulkhan-n plugins/SendGrid/* @richbenwell diff --git a/plugins/Redstor/v1/dataStreams/accountRestores.json b/plugins/Redstor/v1/dataStreams/accountRestores.json index 9a270309..b72eb250 100644 --- a/plugins/Redstor/v1/dataStreams/accountRestores.json +++ b/plugins/Redstor/v1/dataStreams/accountRestores.json @@ -22,7 +22,21 @@ "matches": { "sourceType": { "type": "oneOf", "values": ["Account"] } }, "metadata": [ { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, - { "name": "status", "displayName": "Status Code", "shape": "number" }, + { + "name": "status", + "displayName": "Status", + "valueExpression": "{{ ({ 0: 'Failed', 1: 'Completed successfully', 2: 'Completed with warnings', 3: 'Completed with errors' })[$['status']] ?? $['status'] }}", + "shape": [ + "state", + { + "map": { + "success": ["Completed successfully"], + "warning": ["Completed with warnings"], + "error": ["Completed with errors", "Failed"] + } + } + ] + }, { "name": "accountName", "displayName": "Account", "shape": "string", "visible": false }, { "name": "accountId", "displayName": "Account ID", "shape": "guid", "visible": false }, { "name": "productId", "displayName": "Service ID", "shape": "number", "visible": false } diff --git a/plugins/Redstor/v1/dataStreams/companyRestores.json b/plugins/Redstor/v1/dataStreams/companyRestores.json index a268d57e..fd344b30 100644 --- a/plugins/Redstor/v1/dataStreams/companyRestores.json +++ b/plugins/Redstor/v1/dataStreams/companyRestores.json @@ -27,7 +27,21 @@ "objectPropertyPath": "name" }, { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, - { "name": "status", "displayName": "Status Code", "shape": "number" }, + { + "name": "status", + "displayName": "Status", + "valueExpression": "{{ ({ 0: 'Failed', 1: 'Completed successfully', 2: 'Completed with warnings', 3: 'Completed with errors' })[$['status']] ?? $['status'] }}", + "shape": [ + "state", + { + "map": { + "success": ["Completed successfully"], + "warning": ["Completed with warnings"], + "error": ["Completed with errors", "Failed"] + } + } + ] + }, { "name": "productId", "displayName": "Service ID", "shape": "number", "visible": false }, { "name": "accountId", "displayName": "Account ID", "shape": "guid", "visible": false }, { "name": "companyId", "displayName": "Company ID", "shape": "guid", "visible": false }, diff --git a/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json index 2b52cee8..e48fc884 100644 --- a/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json +++ b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json @@ -454,11 +454,10 @@ }, { "field": "status", - "title": "Status Code", + "title": "Status", "renderer": { - "type": "number" - }, - "align": "right" + "type": "status" + } } ] }, diff --git a/plugins/Redstor/v1/defaultContent/overview.dash.json b/plugins/Redstor/v1/defaultContent/overview.dash.json index f8952634..39cd1237 100644 --- a/plugins/Redstor/v1/defaultContent/overview.dash.json +++ b/plugins/Redstor/v1/defaultContent/overview.dash.json @@ -361,20 +361,6 @@ } } } - }, - { - "i": "bafb42c9-0350-4e60-a1de-00752e8f99db", - "x": 0, - "y": 10, - "w": 4, - "h": 2, - "z": 0, - "moved": false, - "static": false, - "config": { - "title": "", - "description": "" - } } ] } diff --git a/plugins/Redstor/v1/docs/README.md b/plugins/Redstor/v1/docs/README.md index 62bfcc7e..6e4d07a3 100644 --- a/plugins/Redstor/v1/docs/README.md +++ b/plugins/Redstor/v1/docs/README.md @@ -1,15 +1,15 @@ -Monitor your [Redstor](https://www.redstor.com) Partner account in SquaredUp — customer companies, seat and storage consumption, backup and restore health, and product subscriptions all via the [RedAPI](https://www.redstor.com/our-technology/integrations/redapi/). +Monitor a [Redstor](https://www.redstor.com) Partner account in SquaredUp. Customer companies, seat and storage consumption, backup and restore health, and product subscriptions all via the [RedAPI](https://www.redstor.com/our-technology/integrations/redapi/). ## Setup -You will need a RedAPI **service account** with its Client ID and private key, plus your Partner company's ID. +You will need a RedAPI **service account** with its Client ID and private key, plus the Partner company's ID. -> ⚠️ RedAPI is only available to **Partner Admin** users on current Redstor pricing plans — Company Admins and legacy-plan accounts cannot use it. This data source does not cover Redstor's older Storage Platform REST API. +> ⚠️ RedAPI is only available to **Partner Admin** users on current Redstor pricing plans. Company Admins and legacy-plan accounts cannot use it. This data source does not cover Redstor's older Storage Platform REST API. 1. Sign in to [RedApp](https://redapp.redstor.com) as a **Partner Admin**. -2. Note your Partner company's ID — find it under **Company Settings** — and paste it into the **Company ID** field. -3. Go to **RedAPI → Service accounts** and click to add a new service account. Give it a descriptive name and assign it access to your Partner company (and the customers you want visibility into). -4. Under the service account, create a **key**. Redstor generates a Client ID and a JSON Web Key (JWK) file — download the JWK file. +2. Note the Partner company's ID (found under **Company Settings**) and paste it into the **Company ID** field. +3. Go to **RedAPI > Service accounts** and click to add a new service account. Give it a descriptive name and assign it access to the Partner company (and the customers to monitor). +4. Under the service account, create a **key**. Redstor generates a Client ID and a JSON Web Key (JWK) file. Download the JWK file. 5. Copy the **Client ID** into the **Client ID** field. 6. Open the downloaded JWK file (a `.json` file) and paste its full contents, exactly as downloaded, into the **Private key** field. @@ -17,51 +17,51 @@ You will need a RedAPI **service account** with its Client ID and private key, p | Field | What it is | Where to find it | Required | | ----- | ---------- | ---------------- | -------- | -| **Company ID** | Your Redstor Partner company's ID. Scopes every API call to this company and its direct customers. | RedApp → **Company Settings**. | Yes | -| **Client ID** | Identifies the RedAPI service account used to authenticate. | RedApp → **RedAPI → Service accounts** → your service account's key. | Yes | +| **Company ID** | The Redstor Partner company's ID. Scopes every API call to this company and its direct customers. | RedApp > **Company Settings**. | Yes | +| **Client ID** | Identifies the RedAPI service account used to authenticate. | RedApp > **RedAPI > Service accounts** > the service account's key. | Yes | | **Private key** | The JSON Web Key (JWK) paired with the Client ID; signs the request used to obtain access tokens. | Downloaded when the service account's key was created. | Yes | -On save, the data source authenticates and calls your company's profile; an invalid Company ID, Client ID, or Private key fails setup with an authentication error. +On save, the data source authenticates and calls the configured company's profile; an invalid Company ID, Client ID, or Private key fails setup with an authentication error. ## What this data source contains -- **Customer companies** — your Partner company and the customers beneath it. -- **Seat usage and billing exposure** — active, inactive and shared seats, licensed users, data protected and fair use overage, broken down by product for each company. -- **Backup and restore health** — succeeded, warning, failed and missed counts by product, plus the latest run for every individual backup account. -- **Backup accounts** — every protected mailbox, drive, site and machine, with the product and service it belongs to. -- **Subscriptions** — which products each company is subscribed to, and whether each is on trial. +- **Customer companies**: the Partner company and the customers beneath it. +- **Seat usage and billing exposure**: active, inactive and shared seats, licensed users, data protected and fair use overage, broken down by product for each company. +- **Backup and restore health**: succeeded, warning, failed and missed counts by product, plus the latest run for every individual backup account. +- **Backup accounts**: every protected mailbox, drive, site and machine, with the product and service it belongs to. +- **Subscriptions**: which products each company is subscribed to, and whether each is on trial. ## Data streams -**Estate streams — for summarising activity across the whole Partner account (no company selection needed)** +**Estate streams: for summarising activity across the whole Partner account (no company selection needed)** -- **Estate Backup Summary** — backup results by product, split into the Partner company's own totals and a rollup across every customer beneath it. -- **Estate Restore Summary** — restore results by product, split the same way. -- **Estate Consumption** — seat counts, licensed users and data protected, split the same way. -- **Partner Subscriptions** — which products the Partner company itself is subscribed to. Redstor has no equivalent "across all customers" endpoint for subscriptions, so this covers the Partner company only. +- **Estate Backup Summary**: backup results by product, split into the Partner company's own totals and a rollup across every customer beneath it. +- **Estate Restore Summary**: restore results by product, split the same way. +- **Estate Consumption**: seat counts, licensed users and data protected, split the same way. +- **Partner Subscriptions**: which products the Partner company itself is subscribed to. Redstor has no equivalent "across all customers" endpoint for subscriptions, so this covers the Partner company only. -**Company streams — for summarising activity across one company, picked via a scope or dashboard variable** +**Company streams: for summarising activity across one company, picked via a scope or dashboard variable** -- **Company Backup Summary** — backup results by product for a company, counting succeeded, warnings, errors, failures and missed runs. -- **Company Restore Summary** — restore results by product for a company. -- **Company Backups** — backup run history for every account in a company. -- **Company Restores** — the current restore status for every account in a company. -- **Company Accounts** — every backup account in a company, one row per account, with its product, service, storage region and creation date. -- **Company Consumption** — seat counts, licensed users, data protected and fair use overage by product, for a company. -- **Company Subscriptions** — which products a company is subscribed to, and whether each is on trial. +- **Company Backup Summary**: backup results by product for a company, counting succeeded, warnings, errors, failures and missed runs. +- **Company Restore Summary**: restore results by product for a company. +- **Company Backups**: backup run history for every account in a company. +- **Company Restores**: the current restore status for every account in a company. +- **Company Accounts**: every backup account in a company, one row per account, with its product, service, storage region and creation date. +- **Company Consumption**: seat counts, licensed users, data protected and fair use overage by product, for a company. +- **Company Subscriptions**: which products a company is subscribed to, and whether each is on trial. -**Account streams — for tracking down an individual account's history and errors** +**Account streams: for tracking down an individual account's history and errors** -- **Account Backups** — backup run history for a single account, over Redstor's rolling seven day window. -- **Account Restores** — the current restore status for a single account. -- **Account Backup Errors** — backup error and warning messages for a single account. -- **Account Restore Errors** — restore error and warning messages for a single account. +- **Account Backups**: backup run history for a single account, over Redstor's rolling seven day window. +- **Account Restores**: the current restore status for a single account. +- **Account Backup Errors**: backup error and warning messages for a single account. +- **Account Restore Errors**: restore error and warning messages for a single account. ## What gets indexed | Object type | API source | Represents | | ----------- | ---------- | ---------- | -| **Company** | `GET /companies/{companyId}`, `GET /companies/{companyId}/customers` | Your configured Partner company and its direct customer companies. | +| **Company** | `GET /companies/{companyId}`, `GET /companies/{companyId}/customers` | The configured Partner company and its direct customer companies. | | **Account** | `GET /storage/accounts` | A single backup account: one mailbox, drive, site or machine. Not the same as a seat, as described above. | | **Product** | `GET /products` | A product a company can subscribe to and be billed for, such as O365 or Machines. | | **Service** | `GET /products` | A workload within a product, such as Exchange, OneDrive, SharePoint or Teams within O365. | @@ -69,8 +69,8 @@ On save, the data source authenticates and calls your company's profile; an inva ## Notes -- **Accounts that have never run a backup have no error detail** — Redstor only keeps error messages for accounts that have backup history. The Account Backup Errors and Account Restore Errors streams say so rather than returning rows. In testing this applied to roughly a third of a company's accounts. -- **No groups or collections** — Redstor's grouping features are not available through the API and cannot be reported on. -- **Only direct customers are included** — your Partner company and the customers directly beneath it are indexed; deeper reseller-of-reseller chains are not followed. Raise an issue if you require this extra level of detail. -- **Subject to Redstor's fair use throttling** — RedAPI is rate limited and the limits are not published, so very large estates may import slowly. -- **Read-only** — the data source never creates, modifies, or deletes anything in Redstor. +- **Accounts that have never run a backup have no error detail**: Redstor only keeps error messages for accounts that have backup history. The Account Backup Errors and Account Restore Errors streams say so rather than returning rows. In testing this applied to roughly a third of a company's accounts. +- **No groups or collections**: Redstor's grouping features are not available through the API and cannot be reported on. +- **Only direct customers are included**: the Partner company and the customers directly beneath it are indexed; deeper reseller-of-reseller chains are not followed. Raise an issue if you require this extra level of detail. +- **Subject to Redstor's fair use throttling**: RedAPI is rate limited and the limits are not published, so very large estates may import slowly. +- **Read-only**: the data source never creates, modifies, or deletes anything in Redstor. diff --git a/plugins/Redstor/v1/metadata.json b/plugins/Redstor/v1/metadata.json index 93e5e5fe..10bd6cc3 100644 --- a/plugins/Redstor/v1/metadata.json +++ b/plugins/Redstor/v1/metadata.json @@ -3,7 +3,7 @@ "displayName": "Redstor", "version": "1.0.0", "author": { "name": "@TimWheeler-SQUP", "type": "community" }, - "description": "Monitor your Redstor Partner account — customer companies, backup accounts, backup and restore status, storage consumption, and product subscriptions.", + "description": "Monitor Redstor customer companies, backups, restores, consumption and subscriptions.", "category": "Monitoring", "type": "hybrid", "schemaVersion": "2.1", diff --git a/plugins/Redstor/v1/ui.json b/plugins/Redstor/v1/ui.json index fd59ca94..7a3afe68 100644 --- a/plugins/Redstor/v1/ui.json +++ b/plugins/Redstor/v1/ui.json @@ -4,7 +4,7 @@ "name": "entityIdentifier", "label": "Company ID", "placeholder": "e.g. 3fa85f64-5717-4562-b3fc-2c963f66afa6", - "help": "Identifies your Redstor **Partner** company, found under **RedApp → Company Settings** — scopes every request to this company and its direct customers.", + "help": "Use the Partner company ID, found under **Company Settings** in [RedApp](https://redapp.redstor.com)", "validation": { "required": true } }, { @@ -13,17 +13,15 @@ "label": "Client ID", "allowEncryption": true, "placeholder": "Enter the RedAPI service account Client ID", - "help": "Identifies the RedAPI service account used to authenticate, created in RedApp → **RedAPI → Service accounts**.", "validation": { "required": true } }, { - "type": "textarea", + "type": "password", "name": "privateKey", "label": "Private key", - "rows": 12, "allowEncryption": true, "placeholder": "Paste the full JWK JSON downloaded from RedApp", - "help": "The JSON Web Key (JWK) downloaded when you created the service account's key in RedApp → **RedAPI → Service accounts**. Keep this secret — it authenticates as your Partner Admin. Paste the full JSON file contents, exactly as downloaded.", + "help": "Paste the full JWK file contents, exactly as downloaded", "validation": { "required": true } } ] From 4efb924e6a4e4f175a1d4e0387d75e3deea415ca Mon Sep 17 00:00:00 2001 From: Tim Wheeler Date: Thu, 8 Oct 2026 12:13:21 +0100 Subject: [PATCH 21/24] Update plugins/Redstor/v1/dataStreams/estateConsumption.json Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- plugins/Redstor/v1/dataStreams/estateConsumption.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Redstor/v1/dataStreams/estateConsumption.json b/plugins/Redstor/v1/dataStreams/estateConsumption.json index f7165635..48925bd8 100644 --- a/plugins/Redstor/v1/dataStreams/estateConsumption.json +++ b/plugins/Redstor/v1/dataStreams/estateConsumption.json @@ -9,7 +9,7 @@ "endpointPath": "/consumption/summary", "getArgs": [ { "key": "companyId", "value": "{{dataSource.entityIdentifier}}" }, - { "key": "shouldExcludeTrials", "value": "{{excludeTrials || null}}" } + { "key": "shouldExcludeTrials", "value": "{{excludeTrials}}" } ], "postRequestScript": "estateConsumption.js" }, From 66b2901106f7b9a227dd3874508d618b191529d9 Mon Sep 17 00:00:00 2001 From: Tim Wheeler Date: Thu, 8 Oct 2026 15:53:46 +0100 Subject: [PATCH 22/24] Reword Company ID description in Redstor README Co-Authored-By: Claude Sonnet 5.5 --- plugins/Redstor/v1/docs/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/Redstor/v1/docs/README.md b/plugins/Redstor/v1/docs/README.md index 6e4d07a3..f7cd4ebd 100644 --- a/plugins/Redstor/v1/docs/README.md +++ b/plugins/Redstor/v1/docs/README.md @@ -17,8 +17,13 @@ You will need a RedAPI **service account** with its Client ID and private key, p | Field | What it is | Where to find it | Required | | ----- | ---------- | ---------------- | -------- | +<<<<<<< HEAD | **Company ID** | The Redstor Partner company's ID. Scopes every API call to this company and its direct customers. | RedApp > **Company Settings**. | Yes | | **Client ID** | Identifies the RedAPI service account used to authenticate. | RedApp > **RedAPI > Service accounts** > the service account's key. | Yes | +======= +| **Company ID** | Your Redstor company's ID. You can find this in the Redstor application | RedApp → **Company Settings**. | Yes | +| **Client ID** | Identifies the RedAPI service account used to authenticate. | RedApp → **RedAPI → Service accounts** → your service account's key. | Yes | +>>>>>>> 597e93a (Reword Company ID description in Redstor README) | **Private key** | The JSON Web Key (JWK) paired with the Client ID; signs the request used to obtain access tokens. | Downloaded when the service account's key was created. | Yes | On save, the data source authenticates and calls the configured company's profile; an invalid Company ID, Client ID, or Private key fails setup with an authentication error. From 8124dc472004374fef5fc0cf4fff2a7e727cc2ed Mon Sep 17 00:00:00 2001 From: Tim Wheeler Date: Thu, 8 Oct 2026 15:54:21 +0100 Subject: [PATCH 23/24] Fix README conflict markers left in Company ID table Co-Authored-By: Claude Sonnet 5.5 --- plugins/Redstor/v1/docs/README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/plugins/Redstor/v1/docs/README.md b/plugins/Redstor/v1/docs/README.md index f7cd4ebd..15ae3135 100644 --- a/plugins/Redstor/v1/docs/README.md +++ b/plugins/Redstor/v1/docs/README.md @@ -17,13 +17,8 @@ You will need a RedAPI **service account** with its Client ID and private key, p | Field | What it is | Where to find it | Required | | ----- | ---------- | ---------------- | -------- | -<<<<<<< HEAD -| **Company ID** | The Redstor Partner company's ID. Scopes every API call to this company and its direct customers. | RedApp > **Company Settings**. | Yes | +| **Company ID** | Your Redstor company's ID. You can find this in the Redstor application. | RedApp > **Company Settings**. | Yes | | **Client ID** | Identifies the RedAPI service account used to authenticate. | RedApp > **RedAPI > Service accounts** > the service account's key. | Yes | -======= -| **Company ID** | Your Redstor company's ID. You can find this in the Redstor application | RedApp → **Company Settings**. | Yes | -| **Client ID** | Identifies the RedAPI service account used to authenticate. | RedApp → **RedAPI → Service accounts** → your service account's key. | Yes | ->>>>>>> 597e93a (Reword Company ID description in Redstor README) | **Private key** | The JSON Web Key (JWK) paired with the Client ID; signs the request used to obtain access tokens. | Downloaded when the service account's key was created. | Yes | On save, the data source authenticates and calls the configured company's profile; an invalid Company ID, Client ID, or Private key fails setup with an authentication error. From 8a86123be122e2be6c241a8ff22d42cc01d011ee Mon Sep 17 00:00:00 2001 From: Tim Wheeler Date: Thu, 8 Oct 2026 17:34:54 +0100 Subject: [PATCH 24/24] Keep parent product on indexed Redstor Editions Index productId and productName on Edition objects, as Services already do, so Editions retain their parent-product context. Co-Authored-By: Claude Sonnet 5.5 --- plugins/Redstor/v1/indexDefinitions/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Redstor/v1/indexDefinitions/default.json b/plugins/Redstor/v1/indexDefinitions/default.json index 9c8cef4c..8fab2f77 100644 --- a/plugins/Redstor/v1/indexDefinitions/default.json +++ b/plugins/Redstor/v1/indexDefinitions/default.json @@ -52,7 +52,7 @@ "id": "editionId", "name": "editionName", "type": { "value": "Edition" }, - "properties": ["hasTrial"] + "properties": ["productId", "productName", "hasTrial"] }, "dependsOn": ["productServices"] },