diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d689d1fc..707940f8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -18,6 +18,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/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/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/custom_types.json b/plugins/Redstor/v1/custom_types.json new file mode 100644 index 00000000..4d2ec72c --- /dev/null +++ b/plugins/Redstor/v1/custom_types.json @@ -0,0 +1,37 @@ +[ + { + "name": "Redstor Company", + "sourceType": "Company", + "icon": "building", + "singular": "Company", + "plural": "Companies" + }, + { + "name": "Redstor Account", + "sourceType": "Account", + "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 new file mode 100644 index 00000000..54318a37 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/accountBackupErrors.json @@ -0,0 +1,38 @@ +{ + "name": "accountBackupErrors", + "displayName": "Account Backup Errors", + "description": "Backup error and warning messages for an account", + "tags": ["Backup"], + "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", + "errorHandling": { "type": "script", "script": "errorHandling/accountBackupErrors.js" } + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["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": "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 new file mode 100644 index 00000000..03d60ec6 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/accountBackups.json @@ -0,0 +1,37 @@ +{ + "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 }, + { "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 new file mode 100644 index 00000000..918f813a --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/accountRestoreErrors.json @@ -0,0 +1,38 @@ +{ + "name": "accountRestoreErrors", + "displayName": "Account Restore Errors", + "description": "Restore error and warning messages for an account", + "tags": ["Restore"], + "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", + "errorHandling": { "type": "script", "script": "errorHandling/accountRestoreErrors.js" } + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["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": "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 new file mode 100644 index 00000000..b72eb250 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/accountRestores.json @@ -0,0 +1,45 @@ +{ + "name": "accountRestores", + "displayName": "Account Restores", + "description": "Current restore operation status for an account", + "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" } + }, + "pathToData": "results" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Account"] } }, + "metadata": [ + { "name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp" }, + { + "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 } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/backupAccounts.json b/plugins/Redstor/v1/dataStreams/backupAccounts.json new file mode 100644 index 00000000..80107224 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/backupAccounts.json @@ -0,0 +1,33 @@ +{ + "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", + "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" } + }, + "postRequestScript": "backupAccounts.js" + }, + "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" }, + { "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", "shape": "guid", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Redstor/v1/dataStreams/companyBackupSummary.json b/plugins/Redstor/v1/dataStreams/companyBackupSummary.json new file mode 100644 index 00000000..cc244371 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companyBackupSummary.json @@ -0,0 +1,37 @@ +{ + "name": "companyBackupSummary", + "displayName": "Company Backup Summary", + "description": "Current backup operation status by product for a company", + "tags": ["Backup"], + "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": ["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/companyBackups.json b/plugins/Redstor/v1/dataStreams/companyBackups.json new file mode 100644 index 00000000..551605b5 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companyBackups.json @@ -0,0 +1,35 @@ +{ + "name": "companyBackups", + "displayName": "Company Backups", + "description": "Recent backup operation status history per account for a company", + "tags": ["Backup"], + "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": "companyBackups.js" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["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": "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/companyConsumption.json b/plugins/Redstor/v1/dataStreams/companyConsumption.json new file mode 100644 index 00000000..734d241a --- /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": ["Usage"], + "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": ["Company"] } }, + "ui": [ + { + "type": "checkbox", + "name": "excludeTrials", + "label": "Exclude trial subscriptions", + "defaultValue": 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..b2202e28 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companyCustomers.json @@ -0,0 +1,27 @@ +{ + "name": "companyCustomers", + "displayName": "Company Customers", + "description": "Customer companies under the configured Redstor Partner company", + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/companies/{{dataSource.entityIdentifier}}/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..b25d4671 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companyProfile.json @@ -0,0 +1,21 @@ +{ + "name": "companyProfile", + "displayName": "Company Profile", + "description": "The configured Redstor Partner company's own profile", + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/companies/{{dataSource.entityIdentifier}}", + "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/companyRestoreSummary.json b/plugins/Redstor/v1/dataStreams/companyRestoreSummary.json new file mode 100644 index 00000000..da8edcc2 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companyRestoreSummary.json @@ -0,0 +1,36 @@ +{ + "name": "companyRestoreSummary", + "displayName": "Company Restore Summary", + "description": "Current restore operation status by product for a company", + "tags": ["Restore"], + "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": ["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/companyRestores.json b/plugins/Redstor/v1/dataStreams/companyRestores.json new file mode 100644 index 00000000..fd344b30 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companyRestores.json @@ -0,0 +1,51 @@ +{ + "name": "companyRestores", + "displayName": "Company Restores", + "description": "Current restore operation status per account for a company", + "tags": ["Restore"], + "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": ["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", + "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 }, + { "name": "groupId", "displayName": "Group ID", "shape": "string", "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..04344088 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/companySubscriptions.json @@ -0,0 +1,58 @@ +{ + "name": "companySubscriptions", + "displayName": "Company Subscriptions", + "description": "Product subscriptions for a company", + "tags": ["Usage"], + "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": ["Company"] } }, + "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/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..48925bd8 --- /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}}" } + ], + "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/productEditions.json b/plugins/Redstor/v1/dataStreams/productEditions.json new file mode 100644 index 00000000..03308aa9 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/productEditions.json @@ -0,0 +1,27 @@ +{ + "name": "productEditions", + "displayName": "Product Editions", + "description": "Editions available for each Redstor product", + "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", + "visibility": { "type": "hidden" }, + "metadata": [ + { "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..53719a8a --- /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)", + "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", + "visibility": { "type": "hidden" }, + "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..6e40df8d --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/products.json @@ -0,0 +1,24 @@ +{ + "name": "products", + "displayName": "Products", + "description": "Redstor product catalog", + "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", + "visibility": { "type": "hidden" }, + "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/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/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/dataStreams/scripts/companyBackups.js b/plugins/Redstor/v1/dataStreams/scripts/companyBackups.js new file mode 100644 index 00000000..f1843e0d --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/companyBackups.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/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/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/dataStreams/scripts/productEditions.js b/plugins/Redstor/v1/dataStreams/scripts/productEditions.js new file mode 100644 index 00000000..f66d9c75 --- /dev/null +++ b/plugins/Redstor/v1/dataStreams/scripts/productEditions.js @@ -0,0 +1,12 @@ +// GET /products nests editions inside each product — flatten to one row per edition. +const products = data?.results || []; + +result = products.flatMap((product) => + (product.editions || []).map((edition) => ({ + 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/accountPerspective.dash.json b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json new file mode 100644 index 00000000..e48fc884 --- /dev/null +++ b/plugins/Redstor/v1/defaultContent/accountPerspective.dash.json @@ -0,0 +1,476 @@ +{ + "name": "Account View", + "schemaVersion": "1.5", + "timeframe": "none", + "variables": ["{{variables.[Account]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "10564f29-ffb4-4591-b33c-3232a7cf96f8", + "x": 1, + "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]}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count_sum", + "title": "Backup Errors", + "type": "quantitative" + } + ] + }, + "style": { + "scalarLabelPosition": "none" + } + } + } + } + }, + { + "i": "fe0264d0-3511-48d1-9ce1-a860df56c8a4", + "x": 2, + "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]}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count_sum", + "title": "Restore Errors", + "type": "quantitative" + } + ] + }, + "style": { + "scalarLabelPosition": "none" + } + } + } + } + }, + { + "i": "5900ae96-3e88-4e9f-8d0c-f8a90d1da438", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "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]}}" + }, + "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": 0, + "y": 2, + "w": 2, + "h": 3, + "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]}}" + }, + "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 + } + } + } + } + }, + { + "i": "842007ab-6933-49b0-8a6c-cca0473ef35c", + "x": 2, + "y": 2, + "w": 2, + "h": 3, + "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]}}" + }, + "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 + } + } + } + } + }, + { + "i": "2086c180-4d1e-48eb-ace2-6b38858b1e4b", + "x": 0, + "y": 5, + "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]}}" + }, + "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 + } + } + } + } + }, + { + "i": "388195c4-3020-4e54-a3a2-2d5dda9e913d", + "x": 2, + "y": 5, + "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]}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "timestamp", + "title": "Time", + "renderer": { + "type": "date" + } + }, + { + "field": "status", + "title": "Status", + "renderer": { + "type": "status" + } + } + ] + }, + "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..381809ec --- /dev/null +++ b/plugins/Redstor/v1/defaultContent/companyPerspective.dash.json @@ -0,0 +1,443 @@ +{ + "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": 2, + "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": "datastream-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" + } + } + ] + }, + "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 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..39cd1237 --- /dev/null +++ b/plugins/Redstor/v1/defaultContent/overview.dash.json @@ -0,0 +1,367 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "timeframe": "none", + "variables": [], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "8e30bcb6-7300-4281-be83-23bfdbcf639c", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Companies", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "scope": { + "scope": "{{scopes.[Companies]}}", + "workspace": "{{workspaceId}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "title": "Companies", + "field": "count", + "type": "quantitative" + } + ] + }, + "style": { + "scalarLabelPosition": "none" + } + } + } + } + }, + { + "i": "a491a8dd-86aa-432e-82d7-443f26fb4db1", + "x": 1, + "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", + "dataStream": { + "id": "{{dataStreams.estateConsumption}}", + "name": "estateConsumption", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["totalDataProtectedInBytes"] }] } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "totalDataProtectedInBytes_sum", + "type": "quantitative", + "title": "Data Protected" + } + ] + }, + "style": { + "scalarLabelPosition": "none" + } + } + } + } + }, + { + "i": "fbc72ef8-4641-44d4-b815-c60fc5cc3202", + "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", + "dataStream": { + "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"] } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "title": "Product", + "type": "nominal", + "field": "productName_uniqueValues" + } + }, + "style": { + "legendPosition": "bottom", + "colors": [ + "#2BB660", + "#f59e0b", + "#c85f14", + "#c01616", + "#94a3b8" + ], + "cartesianStack": "normalize", + "cartesianXAxisLabels": true, + "cartesianYAxisLabels": true, + "cartesianYAxisScale": "linear", + "cartesianY2AxisScale": "linear" + } + } + } + } + }, + { + "i": "e9def5d0-5073-45f9-8f10-bc8b4aae5695", + "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", + "dataStream": { + "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"] } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "productName_uniqueValues", + "type": "nominal", + "title": "Product" + } + }, + "style": { + "cartesianStack": "normalize", + "colors": [ + "#22c55e", + "#f59e0b", + "#f97316", + "#ef4444" + ], + "legendPosition": "bottom" + } + } + } + } + }, + { + "i": "405ef4a0-1e84-4c8e-b9e6-3a823d83183f", + "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", + "dataStream": { + "id": "{{dataStreams.estateConsumption}}", + "name": "estateConsumption", + "pluginConfigId": "{{configId}}" + }, + "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": {} + } + } + } + }, + { + "i": "c9646d0f-24e0-4ee3-b035-fd2a0654793f", + "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", + "dataStream": { + "id": "{{dataStreams.partnerSubscriptions}}", + "name": "partnerSubscriptions", + "pluginConfigId": "{{configId}}" + }, + "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/scopes.json b/plugins/Redstor/v1/defaultContent/scopes.json new file mode 100644 index 00000000..689f70d0 --- /dev/null +++ b/plugins/Redstor/v1/defaultContent/scopes.json @@ -0,0 +1,26 @@ +[ + { + "name": "Companies", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Company"] } + }, + "variable": { + "name": "Company", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Accounts", + "matches": { + "sourceType": { "type": "oneOf", "values": ["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..15ae3135 --- /dev/null +++ b/plugins/Redstor/v1/docs/README.md @@ -0,0 +1,76 @@ +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 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. + +1. Sign in to [RedApp](https://redapp.redstor.com) as a **Partner Admin**. +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. + +## Configuration fields + +| Field | What it is | Where to find it | Required | +| ----- | ---------- | ---------------- | -------- | +| **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 | +| **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. + +## What this data source contains + +- **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 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. +- **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 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` | 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. | +| **Edition** | `GET /products` | An edition of a product, such as Premium. | + +## 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**: 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/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..8fab2f77 --- /dev/null +++ b/plugins/Redstor/v1/indexDefinitions/default.json @@ -0,0 +1,84 @@ +{ + "steps": [ + { + "name": "companyProfile", + "dataStream": { "name": "companyProfile" }, + "timeframe": "none", + "objectMapping": { + "id": "companyId", + "name": "companyName", + "type": { "value": "Company" }, + "properties": ["companyId", "type", "isEnabled", "parentCompanyId"] + } + }, + { + "name": "companyCustomers", + "dataStream": { "name": "companyCustomers" }, + "timeframe": "none", + "objectMapping": { + "id": "companyId", + "name": "companyName", + "type": { "value": "Company" }, + "properties": ["companyId", "type", "isEnabled", "parentCompanyId"] + } + }, + { + "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"] + }, + "dependsOn": ["products"] + }, + { + "name": "productEditions", + "dataStream": { "name": "productEditions" }, + "timeframe": "none", + "objectMapping": { + "id": "editionId", + "name": "editionName", + "type": { "value": "Edition" }, + "properties": ["productId", "productName", "hasTrial"] + }, + "dependsOn": ["productServices"] + }, + { + "name": "backupAccounts", + "dataStream": { "name": "backupAccounts" }, + "scope": { + "query": "g.V().has(\"sourceType\", \"Company\")" + }, + "timeframe": "none", + "objectMapping": { + "id": "accountId", + "name": "accountName", + "type": { "value": "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..10bd6cc3 --- /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 Redstor customer companies, backups, restores, consumption and subscriptions.", + "category": "Monitoring", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": ["backup", "disaster recovery", "archiving", "msp", "redstor", "redapi"], + "objectTypes": ["Company", "Account", "Product", "Service", "Edition"], + "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": [{ "key": "apiVersion", "value": "2.0" }], + "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..69794c4c --- /dev/null +++ b/plugins/Redstor/v1/preRequest.js @@ -0,0 +1,161 @@ +// 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. +// +// 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 +// exchange (it is short-lived by design, not cached). + +const TOKEN_ENDPOINT = "https://id.redstor.com/connect/token"; + +// --- helpers ----------------------------------------------------------------------- + +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, 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, + 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( + "jwk", + privateKeyJwk, + { 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 privateKeyRaw = String(secrets.privateKey || "").trim(); + + 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, privateKeyJwk); + } catch (e) { + api.report.error( + `Could not sign the Redstor client assertion — check the Private Key is a valid RSA JWK. ${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["Authorization"] = `Bearer ${state.token}`; diff --git a/plugins/Redstor/v1/ui.json b/plugins/Redstor/v1/ui.json new file mode 100644 index 00000000..7a3afe68 --- /dev/null +++ b/plugins/Redstor/v1/ui.json @@ -0,0 +1,27 @@ +[ + { + "type": "text", + "name": "entityIdentifier", + "label": "Company ID", + "placeholder": "e.g. 3fa85f64-5717-4562-b3fc-2c963f66afa6", + "help": "Use the Partner company ID, found under **Company Settings** in [RedApp](https://redapp.redstor.com)", + "validation": { "required": true } + }, + { + "type": "password", + "name": "clientId", + "label": "Client ID", + "allowEncryption": true, + "placeholder": "Enter the RedAPI service account Client ID", + "validation": { "required": true } + }, + { + "type": "password", + "name": "privateKey", + "label": "Private key", + "allowEncryption": true, + "placeholder": "Paste the full JWK JSON downloaded from RedApp", + "help": "Paste the full JWK file contents, exactly as downloaded", + "validation": { "required": true } + } +]