From 4811956d420035e051375dab8a6e83f3125aeee4 Mon Sep 17 00:00:00 2001 From: "EUR\\DIKR" Date: Thu, 1 Aug 2024 14:03:26 +0530 Subject: [PATCH 1/2] Enhancements made according to ISC --- .vscode/settings.json | 6 +- package.json | 2 +- src/models/EplRule.ts | 7 + src/models/analytics-builder-model.ts | 22 ++ .../device-registration-lookup.component.html | 8 + .../device-registration-lookup.component.ts | 22 ++ src/modules/lookup/lookup.module.ts | 3 + ...k-device-registration-modal.component.html | 99 ++++++ ...ulk-device-registration-modal.component.ts | 81 +++++ .../alarm-mapping-provisioning.component.html | 8 +- ...lytics-builder-provisioning.component.html | 33 ++ ...nalytics-builder-provisioning.component.ts | 213 ++++++++++++ .../analytics-builder.service.ts | 70 ++++ .../epl-provisioning.component.html | 32 ++ .../epl-provisioning.component.ts | 215 ++++++++++++ .../epl-provisioning/epl.service.ts | 76 ++++ .../firmware-provision.component.html | 213 ++++++++++++ .../firmware-provision.component.ts | 59 ++++ .../provision-icon.component.ts | 18 + .../firmware-provision-modal.component.html | 120 +++++++ .../firmware-provsion-modal.component.ts | 179 ++++++++++ .../operation-scheduler.component.html | 82 +++++ .../operation-scheduler.component.ts | 257 ++++++++++++++ ...ime.filtering-form-renderer.component.html | 45 +++ ...-time.filtering-form-renderer.component.ts | 54 +++ ...tus.filtering-form-renderer.component.html | 37 ++ ...tatus.filtering-form-renderer.component.ts | 35 ++ .../tenants-list/tenant-list.component.html | 39 +++ .../tenants-list/tenant-list.component.ts | 165 +++++++++ .../firmware-versions.component.html | 45 +++ .../firmware-versions.component.ts | 35 ++ .../firmware-provisioning.component.ts | 14 - .../models/bearer-auth.model.ts | 70 ++++ .../models/custom-basic-auth.model.ts | 14 + .../models/operation.model.ts | 6 + .../global-roles-provisioning.component.html | 44 ++- .../global-roles-provisioning.component.ts | 57 ++- .../role-having-app-modal.component.html | 21 ++ .../role-having-app-modal.component.ts | 19 + ...le-having-permissions-modal.component.html | 21 ++ ...role-having-permissions-modal.component.ts | 19 + .../provisioning-navigator-node.factory.ts | 18 +- .../provisioning/provisioning.module.ts | 46 ++- .../smartrest-provisioning.component.ts | 2 +- ...tenant-options-provisioning.component.html | 10 +- .../tenant-options-provisioning.component.ts | 66 +++- .../firmware-statistics.component.css | 3 + .../firmware-statistics.component.html | 145 ++++++-- .../firmware-statistics.component.ts | 197 ++++++----- src/modules/subtenant-management.module.ts | 6 +- src/services/device-details.service.ts | 90 +---- .../device-registration-details.service.ts | 15 +- src/services/fake-microservice.service.ts | 109 ++++-- src/services/provisioning.service.ts | 326 +++++++++++++++--- 54 files changed, 3263 insertions(+), 335 deletions(-) create mode 100644 src/models/EplRule.ts create mode 100644 src/models/analytics-builder-model.ts create mode 100644 src/modules/lookup/modals/bulk-device-registration-modal/bulk-device-registration-modal.component.html create mode 100644 src/modules/lookup/modals/bulk-device-registration-modal/bulk-device-registration-modal.component.ts create mode 100644 src/modules/provisioning/analytics-builder-provisioning/analytics-builder-provisioning.component.html create mode 100644 src/modules/provisioning/analytics-builder-provisioning/analytics-builder-provisioning.component.ts create mode 100644 src/modules/provisioning/analytics-builder-provisioning/analytics-builder.service.ts create mode 100644 src/modules/provisioning/epl-provisioning/epl-provisioning.component.html create mode 100644 src/modules/provisioning/epl-provisioning/epl-provisioning.component.ts create mode 100644 src/modules/provisioning/epl-provisioning/epl.service.ts create mode 100644 src/modules/provisioning/firmware-provisioning/components/firmware-provision.component.html create mode 100644 src/modules/provisioning/firmware-provisioning/components/firmware-provision.component.ts create mode 100644 src/modules/provisioning/firmware-provisioning/components/icon-component/provision-icon.component.ts create mode 100644 src/modules/provisioning/firmware-provisioning/components/modal/firmware-provision-modal.component.html create mode 100644 src/modules/provisioning/firmware-provisioning/components/modal/firmware-provsion-modal.component.ts create mode 100644 src/modules/provisioning/firmware-provisioning/components/operation-scheduler/operation-scheduler.component.html create mode 100644 src/modules/provisioning/firmware-provisioning/components/operation-scheduler/operation-scheduler.component.ts create mode 100644 src/modules/provisioning/firmware-provisioning/components/tenant-filters/creation-time.filtering-form-renderer.component.html create mode 100644 src/modules/provisioning/firmware-provisioning/components/tenant-filters/creation-time.filtering-form-renderer.component.ts create mode 100644 src/modules/provisioning/firmware-provisioning/components/tenant-filters/status.filtering-form-renderer.component.html create mode 100644 src/modules/provisioning/firmware-provisioning/components/tenant-filters/status.filtering-form-renderer.component.ts create mode 100644 src/modules/provisioning/firmware-provisioning/components/tenants-list/tenant-list.component.html create mode 100644 src/modules/provisioning/firmware-provisioning/components/tenants-list/tenant-list.component.ts create mode 100644 src/modules/provisioning/firmware-provisioning/components/versions-list/firmware-versions.component.html create mode 100644 src/modules/provisioning/firmware-provisioning/components/versions-list/firmware-versions.component.ts create mode 100644 src/modules/provisioning/firmware-provisioning/models/bearer-auth.model.ts create mode 100644 src/modules/provisioning/firmware-provisioning/models/custom-basic-auth.model.ts create mode 100644 src/modules/provisioning/firmware-provisioning/models/operation.model.ts create mode 100644 src/modules/provisioning/global-roles-provisioning/role-having-app-modal/role-having-app-modal.component.html create mode 100644 src/modules/provisioning/global-roles-provisioning/role-having-app-modal/role-having-app-modal.component.ts create mode 100644 src/modules/provisioning/global-roles-provisioning/role-having-permissions-modal/role-having-permissions-modal.component.html create mode 100644 src/modules/provisioning/global-roles-provisioning/role-having-permissions-modal/role-having-permissions-modal.component.ts create mode 100644 src/modules/statistics/firmware-statistics/firmware-statistics.component.css diff --git a/.vscode/settings.json b/.vscode/settings.json index 9b8feeb..9cdce87 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,21 +2,21 @@ "[javascript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "editor.formatOnSave": false }, "[typescript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "editor.formatOnSave": false }, "[json]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "editor.formatOnSave": false } diff --git a/package.json b/package.json index b2ba6f6..ab2045b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.7.2", "description": "Tool for managing subtenants from a c8y management or enterprise tenant", "scripts": { - "start": "c8ycli server", + "start": "c8ycli server -u https://isc.na-dev.inet.com/", "build": "rimraf dist && c8ycli build", "build:ci": "rimraf dist && c8ycli build --ci true ./", "deploy": "c8ycli deploy", diff --git a/src/models/EplRule.ts b/src/models/EplRule.ts new file mode 100644 index 0000000..d68b752 --- /dev/null +++ b/src/models/EplRule.ts @@ -0,0 +1,7 @@ +export class EplRule { + id: number; + name: string; + description: string; + state: string; + contents: string; +} diff --git a/src/models/analytics-builder-model.ts b/src/models/analytics-builder-model.ts new file mode 100644 index 0000000..fcb3f46 --- /dev/null +++ b/src/models/analytics-builder-model.ts @@ -0,0 +1,22 @@ +export class AnalyticsBuilderModel { + id: number; + name: string; + description: string; + state: string; + builderVersion: string; + mode: string; + modeProperties: {}; + builderModel: { + nodeDataArray: any[]; + linkDataArray: any[]; + }; + c8y_analyticsModel: {}; + type: string; + templateParameters: any[]; + userInterfaceProperties: { + displayGrid: boolean; + }; + tags: any[]; + runtimeError: string; + runtimeErrorLocalized: any[]; +} diff --git a/src/modules/lookup/device-registration-lookup/device-registration-lookup.component.html b/src/modules/lookup/device-registration-lookup/device-registration-lookup.component.html index 6e50d19..1761e9f 100644 --- a/src/modules/lookup/device-registration-lookup/device-registration-lookup.component.html +++ b/src/modules/lookup/device-registration-lookup/device-registration-lookup.component.html @@ -1,5 +1,13 @@ Device Registration Lookup + + + + + + + \ No newline at end of file diff --git a/src/modules/lookup/modals/bulk-device-registration-modal/bulk-device-registration-modal.component.ts b/src/modules/lookup/modals/bulk-device-registration-modal/bulk-device-registration-modal.component.ts new file mode 100644 index 0000000..f8c451a --- /dev/null +++ b/src/modules/lookup/modals/bulk-device-registration-modal/bulk-device-registration-modal.component.ts @@ -0,0 +1,81 @@ +import { Component } from '@angular/core'; +import { Client, IDeviceRegistrationBulkResult, IDeviceRegistrationCreate } from '@c8y/client'; +import { AlertService, PickedFiles } from '@c8y/ngx-components'; +import { DeviceRegistrationDetailsService } from '@services/device-registration-details.service'; +import { isEmpty } from 'lodash-es'; +import { BsModalRef } from 'ngx-bootstrap/modal'; +import { Subject } from 'rxjs'; +import { saveAs } from 'file-saver'; + +const fullCsvHeaders: string[] = ['ID', 'CREDENTIALS', 'TYPE', 'NAME']; + +@Component({ + selector: 'ps-bulk-device-registration-modal', + templateUrl: './bulk-device-registration-modal.component.html' +}) +export class BulkDeviceRegistrationModalComponent { + clients: Client[]; + response: Subject; + registration: Partial = {}; + selectedTenant: string; + selectedClient: Client; + bulkResponse: IDeviceRegistrationBulkResult; + file: File; + + autoAccept = false; + + constructor( + private bsModalRef: BsModalRef, + private alertService: AlertService, + private deviceRegistrationDetailsService: DeviceRegistrationDetailsService + ) {} + + onTenantSelect(): void { + if (!this.selectedClient || this.selectedTenant !== this.selectedClient.core.tenant) { + this.selectedClient = this.clients.find((tmp) => tmp.core.tenant === this.selectedTenant); + } + } + + onDismiss(): void { + if (this.response) { + this.response.next(null); + } + this.bsModalRef.hide(); + } + + onFile(dropped: PickedFiles) { + if (!isEmpty(dropped.url)) { + this.file = null; + return; + } else if (dropped.droppedFiles) { + this.file = dropped.droppedFiles[0].file; + return; + } else { + this.file = null; + } + } + + downloadFull() { + return this.download(fullCsvHeaders, 'Full bulk registration - template.csv'); + } + + download(headers: string[], fileName: string) { + const headerRaw = headers.map((header) => `${header}`).join(';'); + const binaryFile = new Blob([headerRaw], { type: 'text/csv' }); + saveAs(binaryFile, fileName); + } + + onSave(): void { + this.deviceRegistrationDetailsService.createBulkRegistrationRequest(this.selectedClient, this.file).then( + (res) => { + this.bulkResponse = res.data; + }, + (error) => { + this.alertService.danger('Failed to create Device Registration.', JSON.stringify(error)); + if (this.response) { + this.response.next(); + } + } + ); + } +} diff --git a/src/modules/provisioning/alarm-mapping-provisioning/alarm-mapping-provisioning.component.html b/src/modules/provisioning/alarm-mapping-provisioning/alarm-mapping-provisioning.component.html index a6b65ca..1b5fdb9 100644 --- a/src/modules/provisioning/alarm-mapping-provisioning/alarm-mapping-provisioning.component.html +++ b/src/modules/provisioning/alarm-mapping-provisioning/alarm-mapping-provisioning.component.html @@ -1,20 +1,20 @@ -Alarm Mapping +Alarm Transformation diff --git a/src/modules/provisioning/analytics-builder-provisioning/analytics-builder-provisioning.component.html b/src/modules/provisioning/analytics-builder-provisioning/analytics-builder-provisioning.component.html new file mode 100644 index 0000000..e0750b7 --- /dev/null +++ b/src/modules/provisioning/analytics-builder-provisioning/analytics-builder-provisioning.component.html @@ -0,0 +1,33 @@ +Analytics Builder + +
+ + + + + + + {{ context.value | humanizeAppName | async + }} + + + + + + +
+ + +
+
+
+
+
\ No newline at end of file diff --git a/src/modules/provisioning/analytics-builder-provisioning/analytics-builder-provisioning.component.ts b/src/modules/provisioning/analytics-builder-provisioning/analytics-builder-provisioning.component.ts new file mode 100644 index 0000000..3d83259 --- /dev/null +++ b/src/modules/provisioning/analytics-builder-provisioning/analytics-builder-provisioning.component.ts @@ -0,0 +1,213 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { AnalyticsBuilderService } from './analytics-builder.service'; +import { + ActionControl, + AlertService, + BulkActionControl, + Column, + ColumnDataType, + DataGridComponent, + ModalService, + Pagination +} from '@c8y/ngx-components'; +import { AnalyticsBuilderModel } from '@models/analytics-builder-model'; +import { Client, ICurrentTenant, TenantService } from '@c8y/client'; +import { TenantSelectionService } from '@modules/shared/tenant-selection/tenant-selection.service'; +import { SubtenantDetailsService } from '@services/subtenant-details.service'; +import { flatMap } from 'lodash-es'; +import { FakeMicroserviceService } from '@services/fake-microservice.service'; + +@Component({ + providers: [AnalyticsBuilderService], + selector: 'ps-analytics-builder-provisioning', + templateUrl: './analytics-builder-provisioning.component.html' +}) +export class AnalyticsBuilderProvisioningComponent implements OnInit { + @ViewChild(DataGridComponent, { static: true }) dataGrid: DataGridComponent; + columns: Column[]; + + tenant: ICurrentTenant; + bulkActionControls: BulkActionControl[] = []; + + subscriptionOngoing = false; + + pagination: Pagination = { + currentPage: 1, + pageSize: 50 + }; + + actions: ActionControl[] = []; + + rows: AnalyticsBuilderModel[] = []; + + constructor( + private analyticsBuilderService: AnalyticsBuilderService, + private tenantService: TenantService, + private tenantSelectionService: TenantSelectionService, + private subtenantService: SubtenantDetailsService, + private c8yModalService: ModalService, + private alertService: AlertService, + private credService: FakeMicroserviceService + ) { + this.columns = this.getDefaultColumns(); + } + + ngOnInit(): void { + this.tenantService.current().then((tenant) => { + this.tenant = tenant.data; + }); + this.analyticsBuilderService.fetchAnalyticsBuilder().then((res) => { + this.rows = res.analyticsBuilderModelRepresentations; + }); + } + + getDefaultColumns(): Column[] { + return [ + { + name: 'id', + header: 'Id', + path: 'id', + dataType: ColumnDataType.Numeric, + sortable: false, + filterable: false, + visible: true + }, + { + name: 'name', + header: 'Name', + path: 'name', + dataType: ColumnDataType.TextShort, + sortable: false, + filterable: false, + visible: true + }, + { + name: 'description', + header: 'Description', + path: 'description', + dataType: ColumnDataType.TextLong, + sortable: false, + filterable: false, + visible: true + }, + { + name: 'state', + header: 'State', + path: 'state', + dataType: ColumnDataType.TextShort, + sortable: false, + filterable: false, + visible: true + }, + { + header: 'Actions', + name: 'actions1', + sortable: false, + filterable: false + // visible: false + } + ]; + } + + async createAnalyticsBuilderModel(models: AnalyticsBuilderModel[]): Promise { + let selectedTenantIds: string[] = []; + const tenants = await this.subtenantService.getTenants(); + try { + selectedTenantIds = await this.tenantSelectionService.getTenantSelection(tenants); + } catch (e) { + return; + } + const credentials = await this.credService.prepareCachedDummyMicroserviceForAllSubtenants(); + const filteredCredentials = credentials.filter((cred) => selectedTenantIds.includes(cred.tenant)); + const clients = await this.credService.createClients(filteredCredentials); + try { + await this.c8yModalService.confirm( + `Deploy Analytics Builder Model(s) `, + `Are you sure that you want to deploy the selected Analytics Builder Models to all selected ${clients.length} subtenants?`, + 'warning' + ); + await this.createAnalyticsBuilderModelsToTenants(models, clients); + } catch (e) {} + } + + private async createAnalyticsBuilderModelsToTenants(models: AnalyticsBuilderModel[], clients: Client[]) { + this.subscriptionOngoing = true; + const promArray = models.map((model) => this.analyticsBuilderService.createModelAtAllTenants(model, clients)); + await Promise.all(promArray).then( + (result) => { + const flatResult = flatMap(result); + this.subscriptionOngoing = false; + const successfullySubscribed = flatResult.filter((tmp) => tmp.status === 201); + if (successfullySubscribed.length) { + this.alertService.success( + `Deployment of model(s) successfully to ${successfullySubscribed.length} subtenants.` + ); + } + const failedToSubscribe = flatResult.filter((tmp) => tmp.status != 201); + if (failedToSubscribe.length) { + this.alertService.warning(`Failed to deploy model(s) to ${failedToSubscribe.length} subtenants.`); + } + const diffInResponses = clients.length * models.length - flatResult.length; + if (diffInResponses) { + this.alertService.info(`${diffInResponses} Analytic Builder Model(s) were already deployed.`); + } + }, + (error) => { + this.subscriptionOngoing = false; + this.alertService.danger('Failed to deploy model(s) to all selected subtenants.', JSON.stringify(error)); + } + ); + this.dataGrid.reload(); + } + + async removeAnalyticsBuilderModels(models: AnalyticsBuilderModel[]): Promise { + let selectedTenantIds: string[] = []; + const tenants = await this.subtenantService.getTenants(); + try { + selectedTenantIds = await this.tenantSelectionService.getTenantSelection(tenants); + } catch (e) { + return; + } + const credentials = await this.credService.prepareCachedDummyMicroserviceForAllSubtenants(); + const filteredCredentials = credentials.filter((cred) => selectedTenantIds.includes(cred.tenant)); + const clients = await this.credService.createClients(filteredCredentials); + try { + await this.c8yModalService.confirm( + `Remove Analytics Builder Model(s)`, + `Are you sure that you want to remove the selected Analytics Builder Model(s) from all selected ${clients.length} subtenants?`, + 'warning' + ); + await this.removeAnalyticsBuilderModelsFromTenants(models, clients); + } catch (e) {} + } + + private async removeAnalyticsBuilderModelsFromTenants(models: AnalyticsBuilderModel[], clients: Client[]) { + this.subscriptionOngoing = true; + const promArray = models.map((model) => this.analyticsBuilderService.removeFromAllTenants(model, clients)); + await Promise.all(promArray).then( + (result) => { + const flatResult = flatMap(result); + this.subscriptionOngoing = false; + const successfullySubscribed = flatResult.filter((tmp) => tmp.status === 204); + if (successfullySubscribed.length) { + this.alertService.success( + `Deletion of model(s) successfully from ${successfullySubscribed.length} subtenants.` + ); + } + const failedToSubscribe = flatResult.filter((tmp) => tmp.status != 204); + if (failedToSubscribe.length) { + this.alertService.warning(`Failed to remove model(s) from ${failedToSubscribe.length} subtenants.`); + } + const diffInResponses = clients.length * models.length - flatResult.length; + if (diffInResponses) { + this.alertService.info(`${diffInResponses} Analytic Builder Model(s) were already removed.`); + } + }, + (error) => { + this.subscriptionOngoing = false; + this.alertService.danger('Failed to deploy model(s) to all selected subtenants.', JSON.stringify(error)); + } + ); + this.dataGrid.reload(); + } +} diff --git a/src/modules/provisioning/analytics-builder-provisioning/analytics-builder.service.ts b/src/modules/provisioning/analytics-builder-provisioning/analytics-builder.service.ts new file mode 100644 index 0000000..6390723 --- /dev/null +++ b/src/modules/provisioning/analytics-builder-provisioning/analytics-builder.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@angular/core'; +import { Client, FetchClient, IFetchOptions, IFetchResponse } from '@c8y/client'; +import { AnalyticsBuilderModel } from '@models/analytics-builder-model'; + +@Injectable() +export class AnalyticsBuilderService { + constructor(private fetch: FetchClient) {} + + GET_OPTIONS: IFetchOptions = { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }; + + async fetchAnalyticsBuilder() { + const response = await this.fetch.fetch('service/cep/analyticsbuilder', this.GET_OPTIONS); + return response.json(); + } + + createModelAtAllTenants(model: AnalyticsBuilderModel, clients: Client[]) { + const promArray = clients.map((client) => this.createAtTenant(model, client)); + return Promise.all(promArray); + } + + createAtTenant(model: AnalyticsBuilderModel, client: Client) { + const options: RequestInit = { + method: 'POST', + headers: { + 'Content-Type': 'application/vnd.com.nsn.cumulocity.applicationReference+json', + Accept: 'application/vnd.com.nsn.cumulocity.applicationReference+json' + }, + body: JSON.stringify(model) + }; + return client.core.fetch('/service/cep/analyticsbuilder', options); + } + + removeFromAllTenants(model: AnalyticsBuilderModel, clients: Client[]): Promise { + const promArray = clients.map((client) => this.removeFromTenant(model, client)); + return Promise.all(promArray); + } + + async removeFromTenant(model: AnalyticsBuilderModel, client: Client) { + const tenantModel = await this.checkIfAnalyticsBuilderModelIsAvailable(model, client); + if (tenantModel) { + const options: RequestInit = { + method: 'DELETE', + headers: { + 'Content-Type': 'application/vnd.com.nsn.cumulocity.applicationReference+json', + Accept: 'application/vnd.com.nsn.cumulocity.applicationReference+json' + } + }; + return client.core.fetch('/service/cep/analyticsbuilder/' + tenantModel.id, options); + } + } + + async checkIfAnalyticsBuilderModelIsAvailable( + model: AnalyticsBuilderModel, + client: Client + ): Promise { + const response = await client.core.fetch('/service/cep/analyticsbuilder', this.GET_OPTIONS); + if (response.status === 200) { + const models = await response.json(); + return models.analyticsBuilderModelRepresentations.find((tmp) => tmp.name === model.name); + } else { + console.warn('Model :' + model.name + ' not found in tenant: ' + client.core.tenant); + return null; + } + } +} diff --git a/src/modules/provisioning/epl-provisioning/epl-provisioning.component.html b/src/modules/provisioning/epl-provisioning/epl-provisioning.component.html new file mode 100644 index 0000000..1b739f5 --- /dev/null +++ b/src/modules/provisioning/epl-provisioning/epl-provisioning.component.html @@ -0,0 +1,32 @@ +EPL Rules + +
+ + + + + + {{ context.value | humanizeAppName | async + }} + + + + + + +
+ + +
+
+
+
+
\ No newline at end of file diff --git a/src/modules/provisioning/epl-provisioning/epl-provisioning.component.ts b/src/modules/provisioning/epl-provisioning/epl-provisioning.component.ts new file mode 100644 index 0000000..a3c06ce --- /dev/null +++ b/src/modules/provisioning/epl-provisioning/epl-provisioning.component.ts @@ -0,0 +1,215 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { + ActionControl, + AlertService, + BulkActionControl, + Column, + ColumnDataType, + DataGridComponent, + ModalService, + Pagination +} from '@c8y/ngx-components'; +import { EplService } from './epl.service'; +import { flatMap } from 'lodash-es'; +import { EplRule } from '@models/EplRule'; +import { Client, ICurrentTenant, TenantService } from '@c8y/client'; +import { TenantSelectionService } from '@modules/shared/tenant-selection/tenant-selection.service'; +import { SubtenantDetailsService } from '@services/subtenant-details.service'; +import { FakeMicroserviceService } from '@services/fake-microservice.service'; + +@Component({ + providers: [EplService], + selector: 'ps-epl-provisioning', + templateUrl: './epl-provisioning.component.html' +}) +export class EplProvisioningComponent implements OnInit { + @ViewChild(DataGridComponent, { static: true }) dataGrid: DataGridComponent; + columns: Column[]; + + currentTenantId: string; + tenant: ICurrentTenant; + + bulkActionControls: BulkActionControl[] = []; + + subscriptionOngoing = false; + + pagination: Pagination = { + currentPage: 1, + pageSize: 50 + }; + + actions: ActionControl[] = []; + + rows: EplRule[] = []; + + constructor( + private eplService: EplService, + private tenantService: TenantService, + private tenantSelectionService: TenantSelectionService, + private subtenantService: SubtenantDetailsService, + private c8yModalService: ModalService, + private alertService: AlertService, + private credService: FakeMicroserviceService + ) { + this.columns = this.getDefaultColumns(); + } + + ngOnInit(): void { + this.tenantService.current().then((tenant) => { + this.tenant = tenant.data; + }); + this.eplService.getEplFiles().then((res) => { + this.rows = res.eplfiles; + }); + } + + getDefaultColumns(): Column[] { + return [ + { + name: 'id', + header: 'Id', + path: 'id', + dataType: ColumnDataType.Numeric, + sortable: false, + filterable: false, + visible: true + }, + { + name: 'name', + header: 'Name', + path: 'name', + dataType: ColumnDataType.TextShort, + sortable: false, + filterable: false, + visible: true + }, + { + name: 'description', + header: 'Description', + path: 'description', + dataType: ColumnDataType.TextLong, + sortable: false, + filterable: false, + visible: true + }, + { + name: 'state', + header: 'State', + path: 'state', + dataType: ColumnDataType.TextShort, + sortable: false, + filterable: false, + visible: true + }, + { + header: 'Actions', + name: 'actions1', + sortable: false, + filterable: false + // visible: false + } + ]; + } + + async deployEplRule(rules: EplRule[]): Promise { + let selectedTenantIds: string[] = []; + const tenants = await this.subtenantService.getTenants(); + try { + selectedTenantIds = await this.tenantSelectionService.getTenantSelection(tenants); + } catch (e) { + return; + } + const credentials = await this.credService.prepareCachedDummyMicroserviceForAllSubtenants(); + const filteredCredentials = credentials.filter((cred) => selectedTenantIds.includes(cred.tenant)); + const clients = await this.credService.createClients(filteredCredentials); + try { + await this.c8yModalService.confirm( + `Deploy EPL rule(s)`, + `Are you sure that you want to deploy the selected EPL rule(s) to all selected ${clients.length} subtenants?`, + 'warning' + ); + await this.deployEplRuleToTenants(rules, clients); + } catch (e) {} + } + + private async deployEplRuleToTenants(rules: EplRule[], clients: Client[]) { + this.subscriptionOngoing = true; + const promArray = rules.map((rule) => this.eplService.deployToAllTenants(rule, clients)); + await Promise.all(promArray).then( + (result) => { + const flatResult = flatMap(result); + this.subscriptionOngoing = false; + const successfullySubscribed = flatResult.filter((tmp) => tmp.status === 201); + if (successfullySubscribed.length) { + this.alertService.success( + `Deployment of rule(s) successfully to ${successfullySubscribed.length} subtenants.` + ); + } + const failedToSubscribe = flatResult.filter((tmp) => tmp.status != 201); + if (failedToSubscribe.length) { + this.alertService.warning(`Failed to deploy rule(s) to ${failedToSubscribe.length} subtenants.`); + } + const diffInResponses = clients.length * rules.length - flatResult.length; + if (diffInResponses) { + this.alertService.info(`${diffInResponses} epl rule(s) were already deployed.`); + } + }, + (error) => { + this.subscriptionOngoing = false; + this.alertService.danger('Failed to deploy rules(s) to all selected subtenants.', JSON.stringify(error)); + } + ); + this.dataGrid.reload(); + } + + async removeEplRule(rules: EplRule[]): Promise { + let selectedTenantIds: string[] = []; + const tenants = await this.subtenantService.getTenants(); + try { + selectedTenantIds = await this.tenantSelectionService.getTenantSelection(tenants); + } catch (e) { + return; + } + const credentials = await this.credService.prepareCachedDummyMicroserviceForAllSubtenants(); + const filteredCredentials = credentials.filter((cred) => selectedTenantIds.includes(cred.tenant)); + const clients = await this.credService.createClients(filteredCredentials); + try { + await this.c8yModalService.confirm( + `Remove EPL rule(s)`, + `Are you sure that you want to remove the selected EPL rule(s) from all selected ${clients.length} subtenants?`, + 'warning' + ); + await this.removeEplRuleToTenants(rules, clients); + } catch (e) {} + } + + private async removeEplRuleToTenants(rules: EplRule[], clients: Client[]) { + this.subscriptionOngoing = true; + const promArray = rules.map((rule) => this.eplService.removeFromAllTenants(rule, clients)); + await Promise.all(promArray).then( + (result) => { + const flatResult = flatMap(result); + this.subscriptionOngoing = false; + const successfullySubscribed = flatResult.filter((tmp) => tmp.status === 200); + if (successfullySubscribed.length) { + this.alertService.success( + `Deletion of rule(s) successfully from ${successfullySubscribed.length} subtenants.` + ); + } + const failedToSubscribe = flatResult.filter((tmp) => tmp.status != 200); + if (failedToSubscribe.length) { + this.alertService.warning(`Failed to remove rule(s) from ${failedToSubscribe.length} subtenants.`); + } + const diffInResponses = clients.length * rules.length - flatResult.length; + if (diffInResponses) { + this.alertService.info(`${diffInResponses} epl rules were already removed.`); + } + }, + (error) => { + this.subscriptionOngoing = false; + this.alertService.danger('Failed to deploy rules(s) to all selected subtenants.', JSON.stringify(error)); + } + ); + this.dataGrid.reload(); + } +} diff --git a/src/modules/provisioning/epl-provisioning/epl.service.ts b/src/modules/provisioning/epl-provisioning/epl.service.ts new file mode 100644 index 0000000..079bdbe --- /dev/null +++ b/src/modules/provisioning/epl-provisioning/epl.service.ts @@ -0,0 +1,76 @@ +import { Injectable } from '@angular/core'; +import { Client, FetchClient, IFetchOptions, IFetchResponse } from '@c8y/client'; +import { EplRule } from '@models/EplRule'; + +@Injectable() +export class EplService { + constructor(private fetch: FetchClient) {} + + GET_OPTIONS: IFetchOptions = { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }; + + POST_OPTIONS: IFetchOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }; + + async getEplFiles() { + const response = await this.fetch.fetch('service/cep/eplfiles?contents=true', this.GET_OPTIONS); + return response.json(); + } + + deployToAllTenants(rule: EplRule, clients: Client[]): Promise { + const promArray = clients.map((client) => this.deployToTenant(rule, client)); + return Promise.all(promArray); + } + + deployToTenant(rule: EplRule, client: Client) { + const options: RequestInit = { + method: 'POST', + headers: { + 'Content-Type': 'application/vnd.com.nsn.cumulocity.applicationReference+json', + Accept: 'application/vnd.com.nsn.cumulocity.applicationReference+json' + }, + body: JSON.stringify({ + name: rule.name, + description: rule.description, + state: rule.state, + contents: rule.contents + }) + }; + return client.core.fetch('service/cep/eplfiles', options); + } + + removeFromAllTenants(rule: EplRule, clients: Client[]): Promise { + const promArray = clients.map((client) => this.removeFromTenant(rule, client)); + return Promise.all(promArray); + } + + async removeFromTenant(rule: EplRule, client: Client) { + const tenantRule = await this.checkIfEplRuleIsAvailable(rule, client); + if (tenantRule) { + const options: RequestInit = { + method: 'DELETE', + headers: { + 'Content-Type': 'application/vnd.com.nsn.cumulocity.applicationReference+json', + Accept: 'application/vnd.com.nsn.cumulocity.applicationReference+json' + } + }; + return client.core.fetch('service/cep/eplfiles/' + tenantRule.id, options); + } + } + + async checkIfEplRuleIsAvailable(rule: EplRule, client: Client): Promise { + const response = await client.core.fetch('service/cep/eplfiles', this.GET_OPTIONS); + if (response.status === 200) { + const rules = await response.json(); + return rules.eplfiles.find((tmp) => tmp.name === rule.name); + } + } +} diff --git a/src/modules/provisioning/firmware-provisioning/components/firmware-provision.component.html b/src/modules/provisioning/firmware-provisioning/components/firmware-provision.component.html new file mode 100644 index 0000000..8df20ae --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/components/firmware-provision.component.html @@ -0,0 +1,213 @@ + + {{ pageTitle | translate }} + + + + + + + + + + + + + + + + + +
+

+

No firmwares to display.

+
+ + +
+

+

No results to display.

+

Refine your search terms or check your spelling.

+
+ + + + + + +
+ + + +
+
+

+ + {{ "Description" | translate }} + + + + +

+ + + {{ "No description" | translate }} + + +
+
+
+ Device type + + + + + + {{ "Undefined" | translate }} + + +
+
+
+ + + + + No versions + + + + + 1 version + + + + + + {{ count }} versions + + + + + +
+
+ +
+
+ + {{ "Provision" | translate }} + + + + {{ "De-Provision" | translate }} + +
+
+ +
+
+ +
+
diff --git a/src/modules/provisioning/firmware-provisioning/components/firmware-provision.component.ts b/src/modules/provisioning/firmware-provisioning/components/firmware-provision.component.ts new file mode 100644 index 0000000..364ea73 --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/components/firmware-provision.component.ts @@ -0,0 +1,59 @@ +import { Component } from '@angular/core'; +import { IResultList, IManagedObject } from '@c8y/client'; +import { BehaviorSubject, Observable, combineLatest } from 'rxjs'; +import { debounceTime, distinctUntilChanged, shareReplay, switchMap, tap } from 'rxjs/operators'; +import { AdvancedSoftwareService, RepositoryService, RepositoryType } from '@c8y/ngx-components/repository/shared'; +import { OperationRealtimeService } from '@c8y/ngx-components'; + +@Component({ + providers: [RepositoryService, OperationRealtimeService, AdvancedSoftwareService], + selector: 'inet-firmware-provision', + templateUrl: 'firmware-provision.component.html' +}) +export class FirmwareProvisionComponent { + pageTitle: string = 'Firmware Provisioning'; + + textFilter$: BehaviorSubject = new BehaviorSubject(''); + reload$: BehaviorSubject = new BehaviorSubject(null); + reloading: boolean = false; + firmwares$: Observable> = combineLatest( + this.textFilter$.pipe(debounceTime(400), distinctUntilChanged()), + this.reload$ + ).pipe( + tap(() => { + this.reloading = true; + }), + switchMap(([text]) => this.getFirmwares(text)), + tap(() => { + this.reloading = false; + }), + shareReplay(1) + ); + + showFirmwareProvisionModal: boolean = false; + firmwareProvisionModalTitle: string = 'Firmware Provisioning'; + firmwareProvisionModalFirmware: IManagedObject; + firmwareProvisionModalProvisionFirmware: boolean = true; + + constructor(private repositoryService: RepositoryService) {} + + getFirmwares(partialText?: string) { + const properties: string[] = ['name', 'description', 'c8y_Filter.type']; + const partialTextFilter = { partialText, properties }; + return this.repositoryService.listRepositoryEntries(RepositoryType.FIRMWARE, { + partialTextFilter + }); + } + + provisionFirmware(firmware: IManagedObject, provisionFirmware: boolean) { + this.showFirmwareProvisionModal = true; + this.firmwareProvisionModalTitle = provisionFirmware ? 'Firmware Provisioning' : 'Firmware De-Provisioning'; + this.firmwareProvisionModalFirmware = firmware; + this.firmwareProvisionModalProvisionFirmware = provisionFirmware; + } + + closeFirmwareProvisionModal() { + this.pageTitle = 'Firmware Provisioning'; + this.showFirmwareProvisionModal = false; + } +} diff --git a/src/modules/provisioning/firmware-provisioning/components/icon-component/provision-icon.component.ts b/src/modules/provisioning/firmware-provisioning/components/icon-component/provision-icon.component.ts new file mode 100644 index 0000000..016aab6 --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/components/icon-component/provision-icon.component.ts @@ -0,0 +1,18 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "provision-icon", + template: ` Provision Icon`, +}) +export class ProvisionIconComponent { + imageSource = require("/assets/icf_asset-management.svg"); + /** + * Uncomment this line and comment the above line to use the image from the assets folder in local environment + imageSource = require("../../../../../assets/icf_asset-management.svg"); + */ + constructor() {} +} diff --git a/src/modules/provisioning/firmware-provisioning/components/modal/firmware-provision-modal.component.html b/src/modules/provisioning/firmware-provisioning/components/modal/firmware-provision-modal.component.html new file mode 100644 index 0000000..1820812 --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/components/modal/firmware-provision-modal.component.html @@ -0,0 +1,120 @@ + + {{ modalTitle | translate }} + + + + +
+
+
+

+ {{ "Select Firmware Version" | translate }} +

+
+
+
+
+ +
+ +
+ +
+
+
+

+ {{ "Select Tenants" | translate }} +

+
+
+
+
+ +
+ +
+ +
+
+
+

+ {{ "Confirm and schedule firmware upgrade operation" | translate }} +

+
+
+
+
+
+ +
+
+ +
+
diff --git a/src/modules/provisioning/firmware-provisioning/components/modal/firmware-provsion-modal.component.ts b/src/modules/provisioning/firmware-provisioning/components/modal/firmware-provsion-modal.component.ts new file mode 100644 index 0000000..f728fed --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/components/modal/firmware-provsion-modal.component.ts @@ -0,0 +1,179 @@ +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { IManagedObject, ITenant } from '@c8y/client'; +import { AlertService, C8yStepper, ModalService } from '@c8y/ngx-components'; +import { OperationSchedulerComponent } from '../operation-scheduler/operation-scheduler.component'; +import { IOperation } from '../../models/operation.model'; +import { CdkStep } from '@angular/cdk/stepper'; +import { FakeMicroserviceService } from '@services/fake-microservice.service'; +import { ProvisioningService } from '@services/provisioning.service'; + +enum step { + FIRST = 0, + SECOND = 1, + THIRD = 2, + FOURTH = 3 +} + +@Component({ + selector: 'inet-firmware-provision-modal', + templateUrl: 'firmware-provision-modal.component.html' +}) +export class FirmwareProvisionModalComponent implements OnInit { + @Input() firmware: IManagedObject; + @Input() modalTitle: string = 'Firmware Provisioning'; + @Input() provisionFirmwareObject: boolean; + + @Output() onCancel: EventEmitter = new EventEmitter(); + + @ViewChild(C8yStepper, { static: true }) + stepper: C8yStepper; + + formGroupStepOne: FormGroup; + formGroupStepTwo: FormGroup; + formGroupStepThree: FormGroup; + + selectedFirmwareVersion: IManagedObject; + selectedTenants: ITenant[] = []; + selectedOperation: IOperation; + pendingFirmwareStatus: boolean = false; + + operationDetails: FormGroup; + + @ViewChild(OperationSchedulerComponent, { static: false }) + operationScheduler: OperationSchedulerComponent; + operationDescription: string; + operationFormName: string; + operationFormDescription: string; + + constructor( + private credService: FakeMicroserviceService, + private provisioning: ProvisioningService, + private alertService: AlertService, + private c8yModalService: ModalService + ) {} + + ngOnInit() {} + + ngAfterViewInit(): void { + if (this.provisionFirmwareObject) { + this.operationDetails = this.operationScheduler.operationForm; + } + } + + /** + * updateSelected is used to set the selected firmware version for provisioning or de-provisioning + */ + updateSelectedFirmwareVersion(version: any) { + this.selectedFirmwareVersion = version; + this.initScheduleForm(); + } + + /** + * initScheduleForm is used to set the schedule form details for automatic firmware upgrade scheduling + */ + initScheduleForm() { + this.operationDescription = `${this.firmware.name} (version ${this.selectedFirmwareVersion.c8y_Firmware.version})`; + this.operationFormName = `Update firmware to: ${this.firmware.name} (version: ${this.selectedFirmwareVersion.c8y_Firmware.version})`; + this.operationFormDescription = `Firmware for hardware revision applied to devices with type ${this.firmware.c8y_Filter?.type}`; + } + + /** + * updateSelectedTenants is used to set the selected tenants for provisioning or de-provisioning + */ + updateSelectedTenants(selectedTenants: any) { + console.log(selectedTenants); + this.selectedTenants = selectedTenants; + } + + /** + * updateOperationDetails is used to set the schedule form details for automatic firmware upgrade scheduling + */ + updateOperationDetails(operationDetails: any) { + this.selectedOperation = operationDetails; + } + + /** + * updateStepper is used to update the stepper + */ + updateStepper($event: { stepper: C8yStepper; step: CdkStep }) { + $event.step.completed = true; + $event.stepper.next(); + } + + /** + * updateFirmware is used to provision or de-provision the firmware to the selected tenants + */ + updateFirmware() { + this.provisionFirmwareObject ? this.provisionFirmware() : this.deProvisionFirmware(); + } + + /** + * provisionFirmware is used to provision the firmware to the selected tenants + * @param selectedTenants + */ + async provisionFirmware(): Promise { + const creds = await this.credService.prepareCachedDummyMicroserviceForAllSubtenants(); + const selectedCreds = creds.filter((c) => this.selectedTenants.map((t) => t.id).includes(c.tenant)); + await this.c8yModalService.confirm( + `Provisioning Firmware`, + `Are you sure that you want to provision the firmware to all selected ${selectedCreds.length} subtenants? This will create a new Firmware on tenants where it did not exist previously. If the same Firmware was already provisioned previously, it's properties will be overwritten.`, + 'warning' + ); + this.pendingFirmwareStatus = true; + const clients = await this.credService.createClients(selectedCreds); + await this.provisioning + .provisionLegacyFirmwareToTenants(clients, this.firmware, this.selectedFirmwareVersion, this.selectedOperation) + .then(() => { + this.pendingFirmwareStatus = false; + this.cancel(); + this.alertService.success(`Provisioned Firmware to ${clients.length} subtenants.`); + }) + .catch((error) => { + this.pendingFirmwareStatus = false; + this.cancel(); + this.alertService.danger('Failed to provision Firmware to all selected subtenants.', JSON.stringify(error)); + }); + } + + /** + * deProvisionFirmware is used to de-provision the firmware to the selected tenants + * @param selectedTenants + */ + async deProvisionFirmware(): Promise { + const creds = await this.credService.prepareCachedDummyMicroserviceForAllSubtenants(); + const selectedCreds = creds.filter((c) => this.selectedTenants.map((t) => t.id).includes(c.tenant)); + await this.c8yModalService.confirm( + `De-Provisioning Firmware`, + `Are you sure that you want to de-provision the firmware to all selected ${selectedCreds.length} subtenants?`, + 'warning' + ); + this.pendingFirmwareStatus = true; + const clients = await this.credService.createClients(selectedCreds); + await this.provisioning + .deprovisionLegacyFirmwareFromTenants(clients, this.firmware, this.selectedFirmwareVersion) + .then(() => { + this.pendingFirmwareStatus = false; + this.cancel(); + this.alertService.success(`De-Provisioned Firmware from ${clients.length} subtenants.`); + }) + .catch((error) => { + this.pendingFirmwareStatus = false; + this.cancel(); + this.alertService.danger( + 'Failed to De-provision Firmware from all selected subtenants.', + JSON.stringify(error) + ); + }); + } + + cancel() { + this.onCancel.emit(); + this.resetStepper(); + } + + private resetStepper() { + this.stepper.reset(); + this.stepper.selectedIndex = 1; + } +} diff --git a/src/modules/provisioning/firmware-provisioning/components/operation-scheduler/operation-scheduler.component.html b/src/modules/provisioning/firmware-provisioning/components/operation-scheduler/operation-scheduler.component.html new file mode 100644 index 0000000..8a7a3b6 --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/components/operation-scheduler/operation-scheduler.component.html @@ -0,0 +1,82 @@ +
+
+
+
+
+

{{ operationTitle | translate }}

+

+ {{ operationDescription | translate }} +

+
+
+
+
+
+
+
+ + + + + + + + + +
+ + + + + + + +
+
+ + +
+ +
+
+
+
+
+
+
diff --git a/src/modules/provisioning/firmware-provisioning/components/operation-scheduler/operation-scheduler.component.ts b/src/modules/provisioning/firmware-provisioning/components/operation-scheduler/operation-scheduler.component.ts new file mode 100644 index 0000000..dc6dabe --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/components/operation-scheduler/operation-scheduler.component.ts @@ -0,0 +1,257 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { IOperation } from '../../models/operation.model'; +import { isEmpty } from 'lodash-es'; +import { Subscription } from 'rxjs'; +import { throttleTime } from 'rxjs/operators'; + +@Component({ + selector: 'inet-operation-scheduler', + templateUrl: 'operation-scheduler.component.html' +}) +export class OperationSchedulerComponent implements ControlValueAccessor, Validator, OnInit { + operationTitle: string = 'Firmware Upgrade'; + + @Input() + operationDescription: string = 'Firmware Upgrade Description'; + + @Input() + operationFormName: string = 'Firmware Upgrade'; + + @Input() + operationFormDescription: string = 'Firmware Upgrade Description'; + + @Output() onOperationUpdate: EventEmitter = new EventEmitter(); + + operationForm: FormGroup; + minDate: Date; + minDelay: number; + delayErrors: ValidationErrors = null; + pickerErrors: ValidationErrors = null; + + private readonly DELAY_SECONDS_DEFAULT: number = 1; + private readonly DELAY_MILLISECONDS_DEFAULT: number = 1; + private readonly MINUTES_AHEAD_DEFAULT: number = 5; + private delaySeconds: number = this.DELAY_SECONDS_DEFAULT; + private delayMilliseconds: number = this.DELAY_MILLISECONDS_DEFAULT; + private minutesAhead: number = this.MINUTES_AHEAD_DEFAULT; + private initialDate: Date; + private delayInSeconds: number; + private currentUnit: string = 'seconds'; + private subscription: Subscription; + + private onChange: (name) => void; + private onTouched: () => void; + private onValidatorChanged: () => void; + + constructor(private formBuilder: FormBuilder) {} + + ngOnInit() { + this.initForm(); + } + + ngOnChanges() { + if (this.operationFormName) { + this.operationForm.patchValue({ + name: this.operationFormName + }); + } + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + initForm() { + this.operationForm = this.formBuilder.group({ + name: ['', Validators.required], + description: [''], + date: ['', Validators.required], + time: ['', [Validators.required, this.timeValidation]], + delay: ['', [Validators.required, Validators.min(this.minDelay)]] + }); + + this.minDate = new Date(); + this.initialDate = new Date(this.minDate.setMinutes(this.minDate.getMinutes() + this.minutesAhead)); + this.minDelay = this.delaySeconds; + + this.operationForm.patchValue({ + name: this.operationFormName, + date: this.initialDate, + time: this.initialDate, + delay: this.delaySeconds + }); + + // Due to the validation of picker and time it could be possible that value changes + // are emitted more than once. Therefore we throttle the emits. + const valueChanges$ = this.operationForm.valueChanges.pipe(throttleTime(100)); + this.subscription = valueChanges$.subscribe((data) => { + this.delayErrors = this.operationForm.controls.delay.errors; + this.pickerErrors = this.operationForm.controls.date.errors; + this.convertDelayHandler(data.unit); + this.emitData(data); + }); + } + + emitData(data: { + name: string; + description: string; + delayInSeconds: number; + date: Date; + time?: Date; + delay?: number; + }) { + if (this.onValidatorChanged) { + this.onValidatorChanged(); + } + + if (data.date && data.time) { + data.date = this.combineDateAndTime(data.date, data.time); + } + + this.convertDelay(this.currentUnit); + data.delayInSeconds = this.delayInSeconds; + + if (this.operationForm.valid) { + const operation: IOperation = { + name: data.name, + description: data.description, + delay: data.delay, + date: data.date + }; + this.onOperationUpdate.emit(operation); + } + } + + markAsTouched(): void { + if (this.onTouched) { + this.onTouched(); + } + } + + writeValue(value: any): void { + if (value) { + this.operationForm.patchValue({ + date: value.scheduledDate, + time: value.scheduledDate, + delay: value.delayInSeconds > 1 ? value.delayInSeconds : value.delayInSeconds * 1000, + unit: value.delayInSeconds > 1 ? 'seconds' : 'milliseconds' + }); + } + } + + validate(): ValidationErrors { + if (this.operationForm.invalid) { + return { + ...this.operationForm.controls.name.errors, + ...this.operationForm.controls.date.errors, + ...this.operationForm.controls.time.errors, + ...this.operationForm.controls.delay.errors + }; + } + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + registerOnValidatorChange(fn: () => void): void { + this.onValidatorChanged = fn; + } + setDisabledState(isDisabled: boolean): void { + isDisabled ? this.operationForm.disable() : this.operationForm.enable(); + } + + convertDelayHandler(unit: string) { + if (this.currentUnit === unit) { + return; + } + + this.currentUnit = unit; + this.convertDelay(this.currentUnit); + + // update validator on delay control to make sure that + // switching from minutes to seconds or vice versa does not harm validation. + this.operationForm.controls.delay.setValidators([Validators.required]); + this.operationForm.controls.delay.updateValueAndValidity(); + } + + private convertDelay(unit: string) { + if (unit && this.operationForm.controls.delay.value) { + this.delayMilliseconds = this.operationForm.controls.delay.value; + if (unit === 'milliseconds') { + this.minDelay = + this.delayMilliseconds > this.DELAY_MILLISECONDS_DEFAULT + ? this.delayMilliseconds + : this.DELAY_MILLISECONDS_DEFAULT; + this.delayInSeconds = this.operationForm.controls.delay.value / 1000; + } else { + this.delaySeconds = this.operationForm.controls.delay.value; + this.minDelay = this.delaySeconds > this.DELAY_SECONDS_DEFAULT ? this.delaySeconds : this.DELAY_SECONDS_DEFAULT; + this.delayInSeconds = this.operationForm.controls.delay.value; + } + } + } + + private combineDateAndTime(date: Date, time: Date) { + return new Date(date.getFullYear(), date.getMonth(), date.getDate(), time.getHours(), time.getMinutes()); + } + + private dateValidation(fControl: FormControl) { + if (fControl.value) { + const date = fControl.value as Date; + fControl.parent.get('time').setValue(date); + return date >= new Date() + ? null + : { + dateValidation: true + }; + } + return { dateValidation: true }; + } + + private timeValidation(fControl: FormControl) { + if (fControl.value) { + const date = fControl.value as Date; + const result = + date >= new Date() + ? null + : { + dateValidation: true + }; + + const picker = fControl.parent.get('date'); + + if (result) { + picker.setErrors(result); + picker.markAsTouched(); + return result; + } + + if (picker && picker.errors && picker.errors.dateValidation) { + delete picker.errors.dateValidation; + + if (isEmpty(picker.errors)) { + picker.setErrors(null); + return result; + } + + picker.setErrors(picker.errors); + } + return result; + } + return { dateValidation: true }; + } +} diff --git a/src/modules/provisioning/firmware-provisioning/components/tenant-filters/creation-time.filtering-form-renderer.component.html b/src/modules/provisioning/firmware-provisioning/components/tenant-filters/creation-time.filtering-form-renderer.component.html new file mode 100644 index 0000000..2dcc796 --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/components/tenant-filters/creation-time.filtering-form-renderer.component.html @@ -0,0 +1,45 @@ +
+
+ + + + + + + +
+
+ + diff --git a/src/modules/provisioning/firmware-provisioning/components/tenant-filters/creation-time.filtering-form-renderer.component.ts b/src/modules/provisioning/firmware-provisioning/components/tenant-filters/creation-time.filtering-form-renderer.component.ts new file mode 100644 index 0000000..70b286d --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/components/tenant-filters/creation-time.filtering-form-renderer.component.ts @@ -0,0 +1,54 @@ +import { Component } from "@angular/core"; +import { ITenant } from "@c8y/client"; +import { FilteringFormRendererContext } from "@c8y/ngx-components"; + +@Component({ + templateUrl: "./creation-time.filtering-form-renderer.component.html", +}) +export class CreationTimeFilteringFormRendererComponent { + model: { + dateFrom: Date; + dateTo: Date; + }; + + constructor(public context: FilteringFormRendererContext) { + this.model = (this.context.property.externalFilterQuery || {}).model || {}; + } + + applyFilter() { + this.context.applyFilter({ + externalFilterQuery: { + model: this.model, + }, + filterPredicate: (tenant: ITenant) => { + const creationTime = new Date(tenant.creationTime); + let dateFrom; + let dateTo; + + if (this.model.dateFrom) { + dateFrom = this.model.dateFrom; + dateFrom.setHours(0, 0, 0, 0); + } + + if (this.model.dateTo) { + dateTo = this.model.dateTo; + dateTo.setHours(23, 59, 59, 999); + } + + return Boolean( + (!dateFrom && !dateTo) || + (dateFrom && !dateTo && dateFrom <= creationTime) || + (!dateFrom && dateTo && creationTime <= dateTo) || + (dateFrom && + dateTo && + dateFrom <= creationTime && + creationTime <= dateTo), + ); + }, + }); + } + + resetFilter() { + this.context.resetFilter(); + } +} diff --git a/src/modules/provisioning/firmware-provisioning/components/tenant-filters/status.filtering-form-renderer.component.html b/src/modules/provisioning/firmware-provisioning/components/tenant-filters/status.filtering-form-renderer.component.html new file mode 100644 index 0000000..af106bd --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/components/tenant-filters/status.filtering-form-renderer.component.html @@ -0,0 +1,37 @@ +
+
+ + + + + + + +
+
+ + diff --git a/src/modules/provisioning/firmware-provisioning/components/tenant-filters/status.filtering-form-renderer.component.ts b/src/modules/provisioning/firmware-provisioning/components/tenant-filters/status.filtering-form-renderer.component.ts new file mode 100644 index 0000000..6dac9dd --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/components/tenant-filters/status.filtering-form-renderer.component.ts @@ -0,0 +1,35 @@ +import { Component } from "@angular/core"; +import { ITenant, TenantStatus } from "@c8y/client"; +import { FilteringFormRendererContext } from "@c8y/ngx-components"; + +@Component({ + templateUrl: "./status.filtering-form-renderer.component.html", +}) +export class StatusFilteringFormRendererComponent { + model: { + active: boolean; + suspended: boolean; + }; + + constructor(public context: FilteringFormRendererContext) { + this.model = (this.context.property.externalFilterQuery || {}).model || {}; + } + + applyFilter() { + this.context.applyFilter({ + externalFilterQuery: { + model: this.model, + }, + filterPredicate: (tenant: ITenant) => + Boolean( + (!this.model.active && !this.model.suspended) || + (this.model.active && tenant.status === TenantStatus.ACTIVE) || + (this.model.suspended && tenant.status === TenantStatus.SUSPENDED), + ), + }); + } + + resetFilter() { + this.context.resetFilter(); + } +} diff --git a/src/modules/provisioning/firmware-provisioning/components/tenants-list/tenant-list.component.html b/src/modules/provisioning/firmware-provisioning/components/tenants-list/tenant-list.component.html new file mode 100644 index 0000000..13392d3 --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/components/tenants-list/tenant-list.component.html @@ -0,0 +1,39 @@ + + + + + {{ context.value | c8yDate }} + + + + + + + + + + + + + + + diff --git a/src/modules/provisioning/firmware-provisioning/components/tenants-list/tenant-list.component.ts b/src/modules/provisioning/firmware-provisioning/components/tenants-list/tenant-list.component.ts new file mode 100644 index 0000000..6979c08 --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/components/tenants-list/tenant-list.component.ts @@ -0,0 +1,165 @@ +import { Component, EventEmitter, OnInit, Output } from "@angular/core"; +import { IResultList, ITenant, TenantService, TenantStatus } from "@c8y/client"; +import { + ActionControl, + Column, + DisplayOptions, + Pagination, + SortOrder, + gettext, +} from "@c8y/ngx-components"; +import { BehaviorSubject, from } from "rxjs"; +import { expand, reduce, shareReplay, takeWhile } from "rxjs/operators"; +import { CreationTimeFilteringFormRendererComponent } from "../tenant-filters/creation-time.filtering-form-renderer.component"; +import { StatusFilteringFormRendererComponent } from "../tenant-filters/status.filtering-form-renderer.component"; + +@Component({ + selector: "inet-tenant-list", + templateUrl: "tenant-list.component.html", +}) +export class TenantListComponent implements OnInit { + tenants$: BehaviorSubject = new BehaviorSubject(undefined); + title: string = null; + loadMoreItemsLabel: string = gettext("Load more tenants"); + loadingItemsLabel: string = gettext("Loading tenants…"); + selectable: boolean = true; + TenantStatus = TenantStatus; + + displayOptions: DisplayOptions = { + bordered: false, + striped: true, + filter: true, + gridHeader: true, + }; + columns: Column[] = this.getColumns(); + pagination: Pagination = this.getPagination(); + showSearch: boolean = true; + actionControls: ActionControl[] = []; + selectedTenantIds: string[] = []; + + @Output() onTenantsSelect: EventEmitter = new EventEmitter< + ITenant[] + >(); + + constructor(private tenantService: TenantService) {} + + ngOnInit() { + this.loadTenants(); + } + + ngOnChanges() { + this.loadTenants(); + } + + /** + * loadTenants is used to load all tenants from the platform for provisioning or de-provisioning + */ + async loadTenants() { + this.tenants$.next(undefined); + from( + this.tenantService.list({ + pageSize: 2000, + withTotalPages: true, + withApps: false, + }), + ) + .pipe( + expand( + (resultList) => + resultList.paging.nextPage !== null && resultList.paging.next(), + ), + takeWhile((resultList) => resultList.paging.nextPage !== null, true), + reduce( + (tenants: ITenant[], resultList: IResultList) => [ + ...tenants, + ...resultList.data, + ], + [], + ), + shareReplay(1), + ) + .subscribe((tenants) => this.tenants$.next(tenants)); + } + + /** + * onItemsSelect is used to set the selected tenants for provisioning or de-provisioning + */ + onItemsSelect(selectedItemIds: string[]) { + let selectedTenants = []; + this.tenants$.subscribe((tenants) => { + tenants.forEach((tenant) => { + if (selectedItemIds.includes(tenant.id)) { + selectedTenants.push(tenant); + } + }); + }); + this.onTenantsSelect.emit(selectedTenants); + } + + getColumns(): Column[] { + return [ + { + name: "company", + header: gettext("Tenant"), + path: "company", + filterable: true, + sortable: true, + sortOrder: "asc" as SortOrder, + }, + { + name: "id", + header: gettext("ID"), + path: "id", + filterable: true, + sortable: true, + }, + { + name: "domain", + header: gettext("Domain"), + path: "domain", + filterable: true, + sortable: true, + }, + { + name: "contactName", + header: gettext("Contact name"), + path: "contactName", + filterable: true, + sortable: true, + }, + { + name: "creationTime", + header: gettext("Created"), + path: "creationTime", + filterable: true, + filteringFormRendererComponent: + CreationTimeFilteringFormRendererComponent, + sortable: true, + }, + { + name: "status", + header: gettext("Status"), + path: "status", + filterable: true, + filteringFormRendererComponent: StatusFilteringFormRendererComponent, + sortable: true, + resizable: false, + }, + ]; + } + + getPagination(): Pagination { + return { + pageSize: 10, + currentPage: 1, + }; + } + + isActive(tenant: ITenant) { + return tenant.status === TenantStatus.ACTIVE; + } + + isSuspended(tenant: ITenant) { + return tenant.status === TenantStatus.SUSPENDED; + } +} diff --git a/src/modules/provisioning/firmware-provisioning/components/versions-list/firmware-versions.component.html b/src/modules/provisioning/firmware-provisioning/components/versions-list/firmware-versions.component.html new file mode 100644 index 0000000..a5d5519 --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/components/versions-list/firmware-versions.component.html @@ -0,0 +1,45 @@ + + + + + + + + +
+

+ {{ baseVersion.c8y_Firmware.version }} + + Latest + +

+
+
+

{{ baseVersion.lastUpdated | c8yDate }}

+
+
+
+
diff --git a/src/modules/provisioning/firmware-provisioning/components/versions-list/firmware-versions.component.ts b/src/modules/provisioning/firmware-provisioning/components/versions-list/firmware-versions.component.ts new file mode 100644 index 0000000..500886c --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/components/versions-list/firmware-versions.component.ts @@ -0,0 +1,35 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { IManagedObject, IResultList } from '@c8y/client'; +import { RepositoryService } from '@c8y/ngx-components/repository/shared'; + +@Component({ + selector: 'inet-firmware-versions', + templateUrl: 'firmware-versions.component.html' +}) +export class FirmwareVersionComponent { + @Input() firmware: IManagedObject; + + @Output() onVersionSelect: EventEmitter = new EventEmitter(); + + constructor(private repositoryService: RepositoryService) {} + + firmwareVersions: IResultList; + + ngOnChanges() { + this.loadFirmwareVersions(); + } + + /** + * loadFirmwareVersions is used to load all firmware versions for the selected firmware + */ + async loadFirmwareVersions() { + this.firmwareVersions = await this.repositoryService.listBaseVersions(this.firmware); + } + + /** + * updateSelected is used to set the selected firmware version for provisioning or de-provisioning + */ + updateSelected(checked, version) { + this.onVersionSelect.emit(version); + } +} diff --git a/src/modules/provisioning/firmware-provisioning/firmware-provisioning.component.ts b/src/modules/provisioning/firmware-provisioning/firmware-provisioning.component.ts index 670cfaf..ed2febf 100644 --- a/src/modules/provisioning/firmware-provisioning/firmware-provisioning.component.ts +++ b/src/modules/provisioning/firmware-provisioning/firmware-provisioning.component.ts @@ -26,18 +26,4 @@ export class FirmwareProvisioningComponent { const { data } = await this.inventory.list(filter); return data; } - - provisionFirmware(firmware: IManagedObject): void { - this.credService.prepareCachedDummyMicroserviceForAllSubtenants().then(async (creds) => { - const clients = await this.credService.createClients(creds); - return this.provisioning.provisionLegacyFirmwareToTenants(clients, firmware); - }); - } - - deprovisioningFirmware(firmware: IManagedObject): void { - this.credService.prepareCachedDummyMicroserviceForAllSubtenants().then(async (creds) => { - const clients = await this.credService.createClients(creds); - return this.provisioning.deprovisionLegacyFirmwareFromTenants(clients, firmware); - }); - } } diff --git a/src/modules/provisioning/firmware-provisioning/models/bearer-auth.model.ts b/src/modules/provisioning/firmware-provisioning/models/bearer-auth.model.ts new file mode 100644 index 0000000..e2d6860 --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/models/bearer-auth.model.ts @@ -0,0 +1,70 @@ +import { BasicAuth, ICredentials } from "@c8y/client"; + +const secrets = new WeakMap(); + +export class BearerAuth extends BasicAuth { + getFetchOptions(options: any): any { + const secret = secrets.get(this); + const { token } = secret; + options.headers = Object.assign( + { Authorization: `Bearer ${token}` }, + options.headers, + ); + return options; + } + + updateCredentials({ tenant, user, password, token, tfa }: ICredentials = {}) { + const secret = secrets.get(this) || {}; + if (user && tenant) { + user = `${tenant}/${user}`; + } + user = user || this.user; + password = password || secret.password; + if (!token && user && password) { + token = btoa(`${user}:${password}`); + } + if (user) { + this.user = user; + } + token = token || secret.token; + tfa = tfa || secret.tfa; + secrets.set(this, { tfa, token, password }); + return token; + } + + getCometdHandshake(config: { ext?: any } = {}) { + const secret = secrets.get(this); + const { token, tfa } = secret; + const KEY = "com.cumulocity.authn"; + const ext = (config.ext = config.ext || {}); + const auth = (ext[KEY] = Object.assign(ext[KEY] || {}, { token, tfa })); + return config; + } + + logout(): void { + delete this.user; + secrets.set(this, {}); + } + + millisecondsUtilTokenExpires(): number { + const secret = secrets.get(this); + const { token } = secret; + try { + const jwt = this.parseJwt(token); + if (jwt && jwt.exp && typeof jwt.exp === "number") { + return jwt.exp * 1000 - new Date().getTime(); + } + } catch (e) { + console.error("Unable to parse JWT"); + } + return 0; + } + + private parseJwt(token: string): any { + try { + return JSON.parse(atob(token.split(".")[1])); + } catch (e) { + return null; + } + } +} diff --git a/src/modules/provisioning/firmware-provisioning/models/custom-basic-auth.model.ts b/src/modules/provisioning/firmware-provisioning/models/custom-basic-auth.model.ts new file mode 100644 index 0000000..7038c29 --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/models/custom-basic-auth.model.ts @@ -0,0 +1,14 @@ +import { BasicAuth } from "@c8y/client"; + +export class CustomBasicAuth extends BasicAuth { + getFetchOptions(options: any): any { + options = super.getFetchOptions(options); + if (options && options.headers && options.headers["X-XSRF-TOKEN"]) { + delete options.headers["X-XSRF-TOKEN"]; + } + if (options && options.headers && options.headers["UseXBasic"]) { + delete options.headers["UseXBasic"]; + } + return options; + } +} diff --git a/src/modules/provisioning/firmware-provisioning/models/operation.model.ts b/src/modules/provisioning/firmware-provisioning/models/operation.model.ts new file mode 100644 index 0000000..09c1a46 --- /dev/null +++ b/src/modules/provisioning/firmware-provisioning/models/operation.model.ts @@ -0,0 +1,6 @@ +export interface IOperation { + name: string; + description: string; + delay: number; + date: Date; +} diff --git a/src/modules/provisioning/global-roles-provisioning/global-roles-provisioning.component.html b/src/modules/provisioning/global-roles-provisioning/global-roles-provisioning.component.html index e9c841e..2d87005 100644 --- a/src/modules/provisioning/global-roles-provisioning/global-roles-provisioning.component.html +++ b/src/modules/provisioning/global-roles-provisioning/global-roles-provisioning.component.html @@ -1,37 +1,53 @@ Global Roles + + + +
- + + + + + + + + - {{ context.value?.references?.length || 0 }} +
{{ context.value?.references?.length || 0 }} + +
- {{ context.value?.length || 0 }} +
{{ context.value?.length || 0 }} + +
- -
diff --git a/src/modules/provisioning/global-roles-provisioning/global-roles-provisioning.component.ts b/src/modules/provisioning/global-roles-provisioning/global-roles-provisioning.component.ts index 5a65d4f..afccd88 100644 --- a/src/modules/provisioning/global-roles-provisioning/global-roles-provisioning.component.ts +++ b/src/modules/provisioning/global-roles-provisioning/global-roles-provisioning.component.ts @@ -1,34 +1,47 @@ -import { Component } from '@angular/core'; -import { IUserGroup } from '@c8y/client'; +import { Component, OnInit } from '@angular/core'; +import { IApplication, ICurrentTenant, IRoleReference, IUserGroup, TenantService } from '@c8y/client'; import { AlertService, Column, ColumnDataType, ModalService } from '@c8y/ngx-components'; import { FakeMicroserviceService } from '@services/fake-microservice.service'; import { GlobalRolesTableDatasourceService } from './global-roles-table-datasource.service'; -import { ProvisioningService } from '@services/provisioning.service'; import { TenantSelectionService } from '@modules/shared/tenant-selection/tenant-selection.service'; +import { RoleHavingPermissionsModalComponent } from './role-having-permissions-modal/role-having-permissions-modal.component'; +import { BsModalService } from 'ngx-bootstrap/modal'; +import { RoleHavingAppModalComponent } from './role-having-app-modal/role-having-app-modal.component'; +import { ProvisioningService } from '@services/provisioning.service'; @Component({ providers: [GlobalRolesTableDatasourceService], selector: 'ps-global-roles-provisioning', templateUrl: './global-roles-provisioning.component.html' }) -export class GlobalRolesProvisioningComponent { +export class GlobalRolesProvisioningComponent implements OnInit { columns: Column[]; + tenant: ICurrentTenant; + constructor( public datasource: GlobalRolesTableDatasourceService, private credService: FakeMicroserviceService, private c8yModalService: ModalService, private alertService: AlertService, private provisioning: ProvisioningService, - private tenantSelectionService: TenantSelectionService + private tenantSelectionService: TenantSelectionService, + private tenantService: TenantService, + private modalService: BsModalService ) { this.columns = this.getDefaultColumns(); } + ngOnInit(): void { + this.tenantService.current().then((tenant) => { + this.tenant = tenant.data; + }); + } + getDefaultColumns(): Column[] { return [ { name: 'name', - header: 'Name', + header: 'role', path: 'name', dataType: ColumnDataType.TextShort, sortable: false, @@ -45,7 +58,7 @@ export class GlobalRolesProvisioningComponent { }, { name: 'roles', - header: 'roles', + header: 'permissions', path: 'roles', dataType: ColumnDataType.TextShort, sortable: false, @@ -114,6 +127,36 @@ export class GlobalRolesProvisioningComponent { } } + openRoleHavingAppModal(role: IUserGroup, apps: IApplication[]): void { + const initialState: Partial = { + role: role, + apps: apps + }; + this.modalService.show(RoleHavingAppModalComponent, { + initialState, + ignoreBackdropClick: true + }); + } + + openTenantsHavingPermissionModal(role: IUserGroup, permissions: IRoleReference[]): void { + const initialState: Partial = { + role: role, + permissions: permissions + }; + this.modalService.show(RoleHavingPermissionsModalComponent, { + initialState, + ignoreBackdropClick: true + }); + } + + createNewGlobalRole() { + window.open('https://' + this.tenant.domainName + '/apps/administration/index.html#/roles/global/new', '_blank'); + } + + navigateToUpdateRole(id: string) { + window.open('https://' + this.tenant.domainName + '/apps/administration/index.html#/roles/global/' + id, '_blank'); + } + async deleteGlobalRole(role: IUserGroup): Promise { try { const credentials = await this.credService.prepareCachedDummyMicroserviceForAllSubtenants(); diff --git a/src/modules/provisioning/global-roles-provisioning/role-having-app-modal/role-having-app-modal.component.html b/src/modules/provisioning/global-roles-provisioning/role-having-app-modal/role-having-app-modal.component.html new file mode 100644 index 0000000..b4a4761 --- /dev/null +++ b/src/modules/provisioning/global-roles-provisioning/role-having-app-modal/role-having-app-modal.component.html @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/src/modules/provisioning/global-roles-provisioning/role-having-app-modal/role-having-app-modal.component.ts b/src/modules/provisioning/global-roles-provisioning/role-having-app-modal/role-having-app-modal.component.ts new file mode 100644 index 0000000..79fbf8c --- /dev/null +++ b/src/modules/provisioning/global-roles-provisioning/role-having-app-modal/role-having-app-modal.component.ts @@ -0,0 +1,19 @@ +import { Input } from '@angular/core'; +import { Component } from '@angular/core'; +import { IApplication, IUserGroup } from '@c8y/client'; +import { BsModalRef } from 'ngx-bootstrap/modal'; + +@Component({ + selector: 'ps-role-having-app-modal', + templateUrl: './role-having-app-modal.component.html' +}) +export class RoleHavingAppModalComponent { + @Input() apps: IApplication[] = []; + @Input() role: IUserGroup; + + constructor(private bsModalRef: BsModalRef) { } + + onDismiss(event: any): void { + this.bsModalRef.hide(); + } +} diff --git a/src/modules/provisioning/global-roles-provisioning/role-having-permissions-modal/role-having-permissions-modal.component.html b/src/modules/provisioning/global-roles-provisioning/role-having-permissions-modal/role-having-permissions-modal.component.html new file mode 100644 index 0000000..275079a --- /dev/null +++ b/src/modules/provisioning/global-roles-provisioning/role-having-permissions-modal/role-having-permissions-modal.component.html @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/src/modules/provisioning/global-roles-provisioning/role-having-permissions-modal/role-having-permissions-modal.component.ts b/src/modules/provisioning/global-roles-provisioning/role-having-permissions-modal/role-having-permissions-modal.component.ts new file mode 100644 index 0000000..7a1eabf --- /dev/null +++ b/src/modules/provisioning/global-roles-provisioning/role-having-permissions-modal/role-having-permissions-modal.component.ts @@ -0,0 +1,19 @@ +import { Input } from '@angular/core'; +import { Component } from '@angular/core'; +import { IRole, IUserGroup } from '@c8y/client'; +import { BsModalRef } from 'ngx-bootstrap/modal'; + +@Component({ + selector: 'ps-role-having-permissions-modal', + templateUrl: './role-having-permissions-modal.component.html' +}) +export class RoleHavingPermissionsModalComponent { + @Input() permissions: IRole[] = []; + @Input() role: IUserGroup; + + constructor(private bsModalRef: BsModalRef) { } + + onDismiss(event: any): void { + this.bsModalRef.hide(); + } +} diff --git a/src/modules/provisioning/provisioning-navigator-node.factory.ts b/src/modules/provisioning/provisioning-navigator-node.factory.ts index fbd8c27..0c1467a 100644 --- a/src/modules/provisioning/provisioning-navigator-node.factory.ts +++ b/src/modules/provisioning/provisioning-navigator-node.factory.ts @@ -11,6 +11,20 @@ export class ProvisioningNavigatorNodeFactory implements NavigatorNodeFactory { path: 'provisioning' }); + const eplNode = new NavigatorNode({ + label: 'EPL Rules', + path: 'provisioning/epl', + icon: 'c8y-apama-epl' + }); + this.provisioningNode.add(eplNode); + + const analyticsModelNode = new NavigatorNode({ + label: 'Analytics Builder', + path: 'provisioning/analytics-builder', + icon: 'c8y-analytics-builder' + }); + this.provisioningNode.add(analyticsModelNode); + const applicationsNode = new NavigatorNode({ label: 'Applications', path: 'provisioning/applications', @@ -61,8 +75,8 @@ export class ProvisioningNavigatorNodeFactory implements NavigatorNodeFactory { this.provisioningNode.add(smartGroupsNode); const alarmMappingNode = new NavigatorNode({ - label: 'Alarm Mapping', - path: 'provisioning/alarm-mapping', + label: 'Alarm Transformation', + path: 'provisioning/alarm-transformation', icon: 'c8y-alarm' }); this.provisioningNode.add(alarmMappingNode); diff --git a/src/modules/provisioning/provisioning.module.ts b/src/modules/provisioning/provisioning.module.ts index d900bf9..86a3fbc 100644 --- a/src/modules/provisioning/provisioning.module.ts +++ b/src/modules/provisioning/provisioning.module.ts @@ -2,7 +2,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { CoreModule, hookNavigator } from '@c8y/ngx-components'; import { ProvisioningNavigatorNodeFactory } from './provisioning-navigator-node.factory'; -import { FirmwareProvisioningComponent } from './firmware-provisioning/firmware-provisioning.component'; import { SmartrestProvisioningComponent } from './smartrest-provisioning/smartrest-provisioning.component'; import { HOOK_MICROSERVICE_ROLE } from '@services/fake-microservice.service'; import { TenantOptionsProvisioningComponent } from './tenant-options-provisioning/tenant-options-provisioning.component'; @@ -17,6 +16,15 @@ import { AlarmMappingProvisioningComponent } from './alarm-mapping-provisioning/ import { RouterModule } from '@angular/router'; import { ApplicationProvisioningComponent } from './application-provisioning/application-provisioning.component'; import { TenantsHavingAppModalComponent } from './application-provisioning/tenants-having-app-modal/tenants-having-app-modal.component'; +import { AnalyticsBuilderProvisioningComponent } from './analytics-builder-provisioning/analytics-builder-provisioning.component'; +import { RoleHavingPermissionsModalComponent } from './global-roles-provisioning/role-having-permissions-modal/role-having-permissions-modal.component'; +import { RoleHavingAppModalComponent } from './global-roles-provisioning/role-having-app-modal/role-having-app-modal.component'; +import { EplProvisioningComponent } from './epl-provisioning/epl-provisioning.component'; +import { FirmwareProvisionComponent } from './firmware-provisioning/components/firmware-provision.component'; +import { FirmwareProvisionModalComponent } from './firmware-provisioning/components/modal/firmware-provsion-modal.component'; +import { FirmwareVersionComponent } from './firmware-provisioning/components/versions-list/firmware-versions.component'; +import { TenantListComponent } from './firmware-provisioning/components/tenants-list/tenant-list.component'; +import { OperationSchedulerComponent } from './firmware-provisioning/components/operation-scheduler/operation-scheduler.component'; @NgModule({ imports: [ @@ -33,13 +41,21 @@ import { TenantsHavingAppModalComponent } from './application-provisioning/tenan redirectTo: 'firmware', pathMatch: 'full' }, + { + path: 'epl', + component: EplProvisioningComponent + }, { path: 'applications', component: ApplicationProvisioningComponent }, + { + path: 'analytics-builder', + component: AnalyticsBuilderProvisioningComponent + }, { path: 'firmware', - component: FirmwareProvisioningComponent + component: FirmwareProvisionComponent }, { path: 'smartrest', @@ -62,7 +78,7 @@ import { TenantsHavingAppModalComponent } from './application-provisioning/tenan component: SmartGroupsProvisioningComponent }, { - path: 'alarm-mapping', + path: 'alarm-transformation', component: AlarmMappingProvisioningComponent } ] @@ -72,7 +88,13 @@ import { TenantsHavingAppModalComponent } from './application-provisioning/tenan declarations: [ ApplicationProvisioningComponent, TenantsHavingAppModalComponent, - FirmwareProvisioningComponent, + RoleHavingPermissionsModalComponent, + RoleHavingAppModalComponent, + FirmwareProvisionComponent, + FirmwareProvisionModalComponent, + FirmwareVersionComponent, + OperationSchedulerComponent, + TenantListComponent, SmartrestProvisioningComponent, TenantOptionsProvisioningComponent, TenantOptionModalComponent, @@ -80,12 +102,20 @@ import { TenantsHavingAppModalComponent } from './application-provisioning/tenan CreateOrEditRetentionRuleModalComponent, GlobalRolesProvisioningComponent, SmartGroupsProvisioningComponent, - AlarmMappingProvisioningComponent + AlarmMappingProvisioningComponent, + EplProvisioningComponent, + AnalyticsBuilderProvisioningComponent ], entryComponents: [ ApplicationProvisioningComponent, TenantsHavingAppModalComponent, - FirmwareProvisioningComponent, + RoleHavingPermissionsModalComponent, + RoleHavingAppModalComponent, + FirmwareProvisionComponent, + FirmwareProvisionModalComponent, + FirmwareVersionComponent, + OperationSchedulerComponent, + TenantListComponent, SmartrestProvisioningComponent, TenantOptionsProvisioningComponent, TenantOptionModalComponent, @@ -93,7 +123,9 @@ import { TenantsHavingAppModalComponent } from './application-provisioning/tenan CreateOrEditRetentionRuleModalComponent, GlobalRolesProvisioningComponent, SmartGroupsProvisioningComponent, - AlarmMappingProvisioningComponent + AlarmMappingProvisioningComponent, + EplProvisioningComponent, + AnalyticsBuilderProvisioningComponent ], providers: [ { diff --git a/src/modules/provisioning/smartrest-provisioning/smartrest-provisioning.component.ts b/src/modules/provisioning/smartrest-provisioning/smartrest-provisioning.component.ts index 60109dc..25d6f20 100644 --- a/src/modules/provisioning/smartrest-provisioning/smartrest-provisioning.component.ts +++ b/src/modules/provisioning/smartrest-provisioning/smartrest-provisioning.component.ts @@ -8,9 +8,9 @@ import { ModalService } from '@c8y/ngx-components'; import { FakeMicroserviceService } from '@services/fake-microservice.service'; -import { ProvisioningService } from '@services/provisioning.service'; import { SmartrestTableDatasourceService } from './smartrest-table-datasource.service'; import { TenantSelectionService } from '@modules/shared/tenant-selection/tenant-selection.service'; +import { ProvisioningService } from '@services/provisioning.service'; @Component({ providers: [SmartrestTableDatasourceService], diff --git a/src/modules/provisioning/tenant-options-provisioning/tenant-options-provisioning.component.html b/src/modules/provisioning/tenant-options-provisioning/tenant-options-provisioning.component.html index e9cc836..d3145d7 100644 --- a/src/modules/provisioning/tenant-options-provisioning/tenant-options-provisioning.component.html +++ b/src/modules/provisioning/tenant-options-provisioning/tenant-options-provisioning.component.html @@ -8,7 +8,7 @@
+ + + + +
diff --git a/src/modules/provisioning/tenant-options-provisioning/tenant-options-provisioning.component.ts b/src/modules/provisioning/tenant-options-provisioning/tenant-options-provisioning.component.ts index c9c5db7..99f2726 100644 --- a/src/modules/provisioning/tenant-options-provisioning/tenant-options-provisioning.component.ts +++ b/src/modules/provisioning/tenant-options-provisioning/tenant-options-provisioning.component.ts @@ -1,22 +1,23 @@ -import { Component } from '@angular/core'; -import { ITenantOption } from '@c8y/client'; +import { Component, OnInit } from '@angular/core'; +import { ICurrentTenant, ITenantOption, TenantService } from '@c8y/client'; import { AlertService, Column, ColumnDataType, ModalService } from '@c8y/ngx-components'; import { TenantSelectionService } from '@modules/shared/tenant-selection/tenant-selection.service'; import { FakeMicroserviceService } from '@services/fake-microservice.service'; -import { ProvisioningService } from '@services/provisioning.service'; import { BsModalService } from 'ngx-bootstrap/modal'; import { Subject } from 'rxjs'; import { take } from 'rxjs/operators'; import { TenantOptionModalComponent } from '../modals/tenant-option-modal/tenant-option-modal.component'; import { TenantOptionsTableDatasourceService } from './tenant-options-table-datasource.service'; +import { ProvisioningService } from '@services/provisioning.service'; @Component({ providers: [TenantOptionsTableDatasourceService], selector: 'ps-tenant-options-provisioning', templateUrl: './tenant-options-provisioning.component.html' }) -export class TenantOptionsProvisioningComponent { +export class TenantOptionsProvisioningComponent implements OnInit { columns: Column[]; + tenant: ICurrentTenant; constructor( private credService: FakeMicroserviceService, @@ -25,10 +26,16 @@ export class TenantOptionsProvisioningComponent { private alertService: AlertService, private provisioning: ProvisioningService, public datasource: TenantOptionsTableDatasourceService, - private tenantSelectionService: TenantSelectionService + private tenantSelectionService: TenantSelectionService, + private tenantService: TenantService ) { this.columns = this.getDefaultColumns(); } + ngOnInit(): void { + this.tenantService.current().then((tenant) => { + this.tenant = tenant.data; + }); + } getDefaultColumns(): Column[] { return [ @@ -73,6 +80,8 @@ export class TenantOptionsProvisioningComponent { try { selectedTenantIds = await this.tenantSelectionService.getTenantSelection(tenantIds); } catch (e) { + this.alertService.warning('Something went wrong during subtenant selection. Please try again.'); + console.warn(e.message); return; } const filteredCredentials = credentials.filter((cred) => selectedTenantIds.includes(cred.tenant)); @@ -95,8 +104,52 @@ export class TenantOptionsProvisioningComponent { ); } ); - } catch (e) {} + } catch (e) { + console.warn(e.message); + } + } catch (e) { + console.warn(e.message); + return; + } + } + + async deprovisionTenantOption(option?: ITenantOption): Promise { + try { + const credentials = await this.credService.prepareCachedDummyMicroserviceForAllSubtenants(); + const tenantIds = credentials.map((tmp) => tmp.tenant); + let selectedTenantIds: string[] = []; + try { + selectedTenantIds = await this.tenantSelectionService.getTenantSelection(tenantIds); + } catch (e) { + this.alertService.warning('Something went wrong during subtenant selection. Please try again.'); + console.warn(e.message); + return; + } + const filteredCredentials = credentials.filter((cred) => selectedTenantIds.includes(cred.tenant)); + + try { + await this.c8yModalService.confirm( + `Deprovisioning Tenant Option`, + `Are you sure that you want to deprovision the Tenant Option to all selected ${filteredCredentials.length} subtenants? This will delete the selected tenant option on tenants in case this option exists.`, + 'warning' + ); + const clients = await this.credService.createClients(filteredCredentials); + await this.provisioning.deprovisionTenantOptionToTenants(clients, option).then( + () => { + this.alertService.success(`Deprovisioned Tenant Option to ${clients.length} subtenants.`); + }, + (error) => { + this.alertService.danger( + 'Failed to deprovision Tenant Option to all selected subtenants.', + JSON.stringify(error) + ); + } + ); + } catch (e) { + console.warn(e.message); + } } catch (e) { + console.warn(e.message); return; } } @@ -125,7 +178,6 @@ export class TenantOptionsProvisioningComponent { } }); initialState.tenantOption = obj; - console.log(obj); } this.modalService.show(TenantOptionModalComponent, { initialState, diff --git a/src/modules/statistics/firmware-statistics/firmware-statistics.component.css b/src/modules/statistics/firmware-statistics/firmware-statistics.component.css new file mode 100644 index 0000000..822709d --- /dev/null +++ b/src/modules/statistics/firmware-statistics/firmware-statistics.component.css @@ -0,0 +1,3 @@ +.firmeware-list .selected, .tenant-list .selected{ + background-color: #f5f5f5; +} \ No newline at end of file diff --git a/src/modules/statistics/firmware-statistics/firmware-statistics.component.html b/src/modules/statistics/firmware-statistics/firmware-statistics.component.html index 5ad93ab..c2d4e04 100644 --- a/src/modules/statistics/firmware-statistics/firmware-statistics.component.html +++ b/src/modules/statistics/firmware-statistics/firmware-statistics.component.html @@ -1,46 +1,125 @@ Firmware Statistics -
-
-
    -
  • -

    - - - {{ chart.label }} - -

    -
  • -
-
+
-
-
-
-

- -

-

{{ "No device/firmware combination selected." | translate }}

+
+
+
+
+

Subtenant

+
+
+ +
+
+
+
+
+
+
+
+ +
+ + {{tenant.id}} + +
+
+ + {{tenant.domain}} + +
+
+
+
-
+
+ +
+
-

{{ currentChart.label }}

+
+

Firmware

+
+
+ +
-
- - +
+
+
+
+
+
+ +
+ + {{firmware.name}} + +
+
+ + {{firmware.description}} + +
+
+
- +
-
+
+
+
+
+

Firmware Statistics

+
+
+
+ {{ "Please select subtenant/firmware combination." | translate }} +
+
+
+ + + + + + {{ chartMessage }} +
+ +
- -
-
-
-
+
\ No newline at end of file diff --git a/src/modules/statistics/firmware-statistics/firmware-statistics.component.ts b/src/modules/statistics/firmware-statistics/firmware-statistics.component.ts index 2e680dc..81dc7ef 100644 --- a/src/modules/statistics/firmware-statistics/firmware-statistics.component.ts +++ b/src/modules/statistics/firmware-statistics/firmware-statistics.component.ts @@ -1,15 +1,21 @@ -import { Component } from '@angular/core'; -import { Router } from '@angular/router'; +import { Component, OnInit } from '@angular/core'; +import { Client, IManagedObject, IResultList, ITenant } from '@c8y/client'; +import { OperationRealtimeService } from '@c8y/ngx-components'; +import { AdvancedSoftwareService, RepositoryService, RepositoryType } from '@c8y/ngx-components/repository/shared'; import { DeviceDetailsService } from '@services/device-details.service'; import { FakeMicroserviceService } from '@services/fake-microservice.service'; -import { flatten } from 'lodash-es'; +import { SubtenantDetailsService } from '@services/subtenant-details.service'; +import { BehaviorSubject } from 'rxjs'; @Component({ + providers: [RepositoryService, OperationRealtimeService, AdvancedSoftwareService], selector: 'ps-firmware-statistics', - templateUrl: './firmware-statistics.component.html' + templateUrl: './firmware-statistics.component.html', + styleUrls: ['./firmware-statistics.component.css'] }) -export class FirmwareStatisticsComponent { - isLoading = true; +export class FirmwareStatisticsComponent implements OnInit { + isLoading = false; + chartMessage: string = ''; charts: { label: string; type: string; @@ -25,102 +31,111 @@ export class FirmwareStatisticsComponent { values: number[]; }; + textFilter$: BehaviorSubject = new BehaviorSubject(''); + textFilterSubtenants$: BehaviorSubject = new BehaviorSubject(''); + reload$: BehaviorSubject = new BehaviorSubject(null); + reloading: boolean = false; + + firmwareVersions: IResultList; + + clients: Client[]; + + tenantSearchString: string; + tenantDetails: ITenant[]; + filteredTenants: ITenant[]; + selectedTenant: ITenant; + + firmwareSearchString: string; + firmwareList: IManagedObject[]; + selectedFirmware: IManagedObject; + constructor( private credService: FakeMicroserviceService, private deviceDetailsService: DeviceDetailsService, - private router: Router - ) { - this.loadData(); + private repositoryService: RepositoryService, + private tenantService: SubtenantDetailsService + ) {} + + async ngOnInit(): Promise { + this.tenantDetails = await this.tenantService.getCachedTenants(); + this.filteredTenants = this.tenantDetails; + this.firmwareList = await this.getFirmwares(''); + + await this.loadClients(); } - loadData(): void { - this.isLoading = true; - this.fetchForPage().then( - (result) => { - this.charts = result.map((tmp) => { - return { - label: tmp.label, - type: tmp.type, - firmwareName: tmp.firmwareName, - labels: tmp.entries.map((entry) => entry.version), - values: tmp.entries.map((entry) => entry.count) - }; - }); - if (this.charts.length) { - this.currentChart = this.charts[0]; - } - this.isLoading = false; - }, - () => { - this.charts = []; - this.currentChart = null; - this.isLoading = false; - } - ); + async getFirmwares(partialText?: string) { + const properties: string[] = ['name', 'description', 'c8y_Filter.type']; + const partialTextFilter = { partialText, properties }; + const result = await this.repositoryService.listRepositoryEntries(RepositoryType.FIRMWARE, { + partialTextFilter + }); + return result.data; } - public selectChart(chart: { - label: string; - type: string; - firmwareName: string; - labels: string[]; - values: number[]; - }): void { - this.currentChart = chart; + resetTenantSearchString() { + this.tenantSearchString = ''; + this.filteredTenants = this.tenantDetails; } - private async fetchForPage(): Promise< - { - label: string; - type: string; - firmwareName: string; - entries: { - version: string; - count: number; - }[]; - }[] - > { - const credentials = await this.credService.prepareCachedDummyMicroserviceForAllSubtenants(); - const clients = await this.credService.createClients(credentials); - const result = await this.deviceDetailsService.getFirmwareStatisticsOfTenants(clients); - const mappedResult = Array.from(result.entries()).map(([type, value]) => { - return Array.from(value.entries()).map(([firmwareName, value2]) => { - const childEntries = Array.from(value2.entries()) - .map(([key2, value3]) => { - return { - version: key2, - count: value3 - }; - }) - .sort((a, b) => b.count - a.count); - return { - label: `${type} - ${firmwareName}`, - type, - firmwareName, - entries: childEntries - }; - }); - }); - return flatten(mappedResult); + onTenantSearch() { + if (this.tenantSearchString.length > 0) { + this.filteredTenants = this.tenantDetails.filter( + (tenant) => + tenant.id.toLowerCase().includes(this.tenantSearchString.toLowerCase()) || + tenant.domain.toLowerCase().includes(this.tenantSearchString.toLowerCase()) + ); + } else { + this.filteredTenants = this.tenantDetails; + } + } + + async resetFirmwareSearchString() { + this.firmwareSearchString = ''; + this.firmwareList = await this.getFirmwares(this.firmwareSearchString); + } + + async onFirmwareSearch() { + this.firmwareList = await this.getFirmwares(this.firmwareSearchString); + } + + onFirmwareSelected(firmware: IManagedObject) { + this.selectedFirmware = firmware; + this.loadChart(); } - pieChartClicked(index: number): void { - const lookupPath = 'lookup'; - const devicePath = 'device'; - const config = this.router.config; - const lookupConfig = config.find((tmp) => tmp.path === lookupPath); - if (lookupConfig && lookupConfig.children && lookupConfig.children.find((tmp) => tmp.path === devicePath)) { - const firmwareVersion = this.currentChart.labels[index]; - const type = this.currentChart.type; - const firmwareName = this.currentChart.firmwareName; - - this.router.navigate([lookupPath, devicePath], { - queryParams: { - firmwareVersion, - type, - firmwareName - } - }); + onTenantSelected(tenant: ITenant) { + this.selectedTenant = tenant; + this.loadChart(); + } + + async loadChart() { + this.isLoading = true; + this.chartMessage = ''; + if (this.selectedFirmware && this.selectedTenant) { + if (!this.clients) { + await this.loadClients(); + } + const client = this.clients.find((cl) => cl.core.tenant === this.selectedTenant.id); + const result = await this.deviceDetailsService.getFirmwareStatistics(client, this.selectedFirmware); + const labels = Array.from(result.keys()); + const values = Array.from(result.values()); + if (values.length > 0 && labels.length > 0) { + this.currentChart = { + label: 'Firmware Statistics', + type: '', + firmwareName: '', + labels: labels, + values: values + }; + } else { + this.chartMessage = 'No device available for the selected firmware and tenant.'; + } } + this.isLoading = false; + } + async loadClients() { + const credentials = await this.credService.prepareCachedDummyMicroserviceForAllSubtenants(); + this.clients = await this.credService.createClients(credentials); } } diff --git a/src/modules/subtenant-management.module.ts b/src/modules/subtenant-management.module.ts index 28e4f93..2b4a885 100644 --- a/src/modules/subtenant-management.module.ts +++ b/src/modules/subtenant-management.module.ts @@ -54,7 +54,11 @@ export class SubtenantManagementModule implements OnDestroy { ]; private rolesTenantUpdate = ['ROLE_TENANT_MANAGEMENT_UPDATE', 'ROLE_TENANT_MANAGEMENT_ADMIN']; - constructor(private appState: AppStateService, private alertService: AlertService, private userService: UserService) { + constructor( + private appState: AppStateService, + private alertService: AlertService, + private userService: UserService + ) { this.roleSubscription = this.appState.currentUser .pipe( filter((user) => !!user), diff --git a/src/services/device-details.service.ts b/src/services/device-details.service.ts index eb06501..8de0327 100644 --- a/src/services/device-details.service.ts +++ b/src/services/device-details.service.ts @@ -6,86 +6,30 @@ import { TenantSpecificDetails } from '@models/tenant-specific-details'; providedIn: 'root' }) export class DeviceDetailsService { - public async getFirmwareStatistics(client: Client): Promise>>> { - const firmwareCounterMap = new Map>>(); + public async getFirmwareStatistics(client: Client, firmware: IManagedObject): Promise> { + const firmwareCounterMap = new Map(); const filter = { - q: '$filter=(has(c8y_Firmware))', + q: `$filter=(c8y_Firmware.name eq '` + firmware.name + `' )`, pageSize: 2000 }; try { - let res = await client.inventory.list(filter); - while (res.data.length > 0) { - res.data.forEach((mo) => { - if (mo && mo.type && mo.c8y_Firmware && mo.c8y_Firmware.name && mo.c8y_Firmware.version) { - const name = mo.c8y_Firmware.name; - const version = mo.c8y_Firmware.version; - const firmwareIdent = `${version}`; - const type = mo.type; - let currentDeviceType = firmwareCounterMap.get(type); - if (!currentDeviceType) { - currentDeviceType = new Map>(); - const currentFirmwareName = new Map(); - currentFirmwareName.set(firmwareIdent, 1); - currentDeviceType.set(name, currentFirmwareName); - firmwareCounterMap.set(type, currentDeviceType); - } else { - let currentFirmwareName = currentDeviceType.get(name); - if (!currentFirmwareName) { - currentFirmwareName = new Map(); - currentDeviceType.set(name, currentFirmwareName); - } - const currentCount = currentFirmwareName.get(firmwareIdent); - if (!currentCount) { - currentFirmwareName.set(firmwareIdent, 1); - } else { - currentFirmwareName.set(firmwareIdent, currentCount + 1); - } - } + const res = await client.inventory.list(filter); + console.log(res); + res.data.forEach((mo) => { + if (mo && mo.type && mo.c8y_Firmware && mo.c8y_Firmware.name && mo.c8y_Firmware.version) { + const version = mo.c8y_Firmware.version; + if (firmwareCounterMap.has(version)) { + const count = firmwareCounterMap.get(version); + firmwareCounterMap.set(version, count + 1); + } else { + firmwareCounterMap.set(version, 1); } - }); - if (res.data.length < filter.pageSize) { - break; } - res = await res.paging.next(filter); - } - } catch (e) {} - - return firmwareCounterMap; - } - - public async getFirmwareStatisticsOfTenants( - clients: Client[] - ): Promise>>> { - const firmwareCounterMap = new Map>>(); - const promArray = clients.map((client) => this.getFirmwareStatistics(client)); - await Promise.all(promArray).then((resArr) => { - resArr.forEach((tmp) => { - tmp.forEach((value, key) => { - let currentDeviceType = firmwareCounterMap.get(key); - if (!currentDeviceType) { - currentDeviceType = new Map>(); - firmwareCounterMap.set(key, currentDeviceType); - } - value.forEach((value2, key2) => { - let currentFirmwareName = currentDeviceType.get(key2); - if (!currentFirmwareName) { - currentFirmwareName = new Map(); - currentDeviceType.set(key2, currentFirmwareName); - } - - value2.forEach((value3, key3) => { - let currentCount = currentFirmwareName.get(key3); - if (currentCount) { - currentCount = currentCount + value3; - } else { - currentCount = value3; - } - currentFirmwareName.set(key3, currentCount); - }); - }); - }); }); - }); + } catch (e) { + console.error(e); + } + return firmwareCounterMap; } diff --git a/src/services/device-registration-details.service.ts b/src/services/device-registration-details.service.ts index 375c97f..2ba6d73 100644 --- a/src/services/device-registration-details.service.ts +++ b/src/services/device-registration-details.service.ts @@ -1,5 +1,11 @@ import { Injectable } from '@angular/core'; -import { Client, IDeviceRegistration, IDeviceRegistrationCreate, IResult } from '@c8y/client'; +import { + Client, + IDeviceRegistration, + IDeviceRegistrationBulkResult, + IDeviceRegistrationCreate, + IResult +} from '@c8y/client'; import { TenantSpecificDetails } from '@models/tenant-specific-details'; @Injectable({ @@ -22,6 +28,13 @@ export class DeviceRegistrationDetailsService { return request; } + public async createBulkRegistrationRequest( + client: Client, + file: File + ): Promise> { + return client.deviceRegistrationBulk.create(file); + } + public acceptRegistrationRequest(client: Client, id: string): Promise> { return client.deviceRegistration.accept(id); } diff --git a/src/services/fake-microservice.service.ts b/src/services/fake-microservice.service.ts index 85fabbe..859ed9f 100644 --- a/src/services/fake-microservice.service.ts +++ b/src/services/fake-microservice.service.ts @@ -17,10 +17,10 @@ import { flatMap, get, omit, uniq } from 'lodash-es'; import { CustomApiService } from './custom-api.service'; import { SubtenantDetailsService } from './subtenant-details.service'; import { ApplicationSubscriptionService } from './application-subscription.service'; -import { TenantSelectionService } from '@modules/shared/tenant-selection/tenant-selection.service'; -import { BearerAuth } from '@models/BearerAuth'; import { interval, Subscription } from 'rxjs'; +import { BearerAuth } from '@models/BearerAuth'; import { CustomBasicAuth } from '@models/CustomBasicAuth'; +import { TenantSelectionService } from '@modules/shared/tenant-selection/tenant-selection.service'; export const HOOK_MICROSERVICE_ROLE = new InjectionToken('MicroserviceRole'); @@ -31,26 +31,27 @@ export class FakeMicroserviceService implements OnDestroy { private requiredRoles: string[] = []; private credentialsCache: Promise; - private cachedModal: Promise; private clientsPromiseCache = new Map>(); private clientsAuthCache = new Map(); private clientsCredentialsCache = new Map(); - + private cachedModal: Promise; private oauthTokenExpirySub: Subscription; constructor( - @Optional() @Inject(HOOK_MICROSERVICE_ROLE) factories: (string | string[])[], + @Optional() + @Inject(HOOK_MICROSERVICE_ROLE) + factories: (string | string[])[], private fetchClient: FetchClient, private appService: ApplicationService, private modalService: ModalService, private customApiService: CustomApiService, private subtenantDetails: SubtenantDetailsService, private applicationSubscription: ApplicationSubscriptionService, - private tenantSelectionService: TenantSelectionService, private appState: AppStateService, private alertService: AlertService, private options: OptionsService, - private loginService: LoginService + private loginService: LoginService, + private tenantSelectionService: TenantSelectionService ) { if (factories) { const roles = flatMap(factories); @@ -153,6 +154,12 @@ export class FakeMicroserviceService implements OnDestroy { throw new Error('Too many retries'); } + /** + * CreateClient creates a client for the given tenant credentials. + * @param credentials + * @param domain + * @returns + */ private async createClient(credentials: ICredentials, domain?: string): Promise { this.clientsCredentialsCache.set(credentials.tenant, credentials); let auth: BasicAuth | BearerAuth; @@ -171,6 +178,9 @@ export class FakeMicroserviceService implements OnDestroy { return client; } + /** + * GetAccessToken retrieves an access token for the given credentials. + */ private async getAccessToken(credentials: ICredentials, domain?: string): Promise { const params = new URLSearchParams({ grant_type: 'PASSWORD', @@ -204,9 +214,9 @@ export class FakeMicroserviceService implements OnDestroy { return json.access_token; } - public async prepareCachedDummyMicroserviceForAllSubtenants(baseUrl?: string): Promise { + public async prepareCachedDummyMicroserviceForAllSubtenants(): Promise { if (!this.credentialsCache) { - this.credentialsCache = this.prepareDummyMicroserviceForAllSubtenants(baseUrl); + this.credentialsCache = this.prepareDummyMicroserviceForAllSubtenants(); } try { return await this.credentialsCache; @@ -216,26 +226,36 @@ export class FakeMicroserviceService implements OnDestroy { } } - public async prepareDummyMicroserviceForAllSubtenants(baseUrl?: string): Promise { + /** + * PrepareDummyMicroserviceForAllSubtenants prepares a dummy microservice for all subtenants. + * @param selectedTenants + * @param baseUrl + * @returns + */ + public async prepareDummyMicroserviceForAllSubtenants(): Promise { const tenantPromise = this.subtenantDetails.getTenants(); if (this.showWarnings()) { await this.checkDataUsageConfirmed(); } const app = await this.createDummyMicroserviceIfNotExisting(); + const tenants = await tenantPromise; let filteredTenants = tenants; if (this.showWarnings()) { filteredTenants = await this.subsetOfTenantsSelected(tenants); } - await this.applicationSubscription.subscribeAppToAllTenants(app, filteredTenants); const bootstrapCredentials = await this.getBootstrapUser(app); - const subscriptions = await this.getMicroserviceSubscriptions(bootstrapCredentials, baseUrl); + const subscriptions = await this.getMicroserviceSubscriptions(bootstrapCredentials); const filteredTenantIds = filteredTenants.map((tmp) => tmp.id); const filteredSubscriptions = subscriptions.filter((tmp) => filteredTenantIds.includes(tmp.tenant)); return filteredSubscriptions; } + /** + * CleanUp is used to unsubscribe the dummy microservice from all subtenants and delete it. + * @returns + */ public async cleanup(): Promise { const app = await this.findDummyMicroservice(); if (!app) { @@ -247,23 +267,40 @@ export class FakeMicroserviceService implements OnDestroy { this.credentialsCache = null; } + /** + * GetMsKey is used to get the microservice key for subtenant-mgmt microservice. + * @returns string + */ public async getMsKey(): Promise { const currentTenant = this.appState.currentTenant.value; const hashedTenantId = await this.sha256(currentTenant.name); return `subtenant-mgmt-${hashedTenantId.substring(0, 8)}`; } + /** + * GetMsName is used to get the microservice name for subtenant-mgmt microservice. + * @returns string + */ public async getMsName(): Promise { const currentTenant = this.appState.currentTenant.value; const hashedTenantId = await this.sha256(currentTenant.name); return `subtenant-mgmt-${hashedTenantId.substring(0, 8)}`; } + /** + * GetMsDescription is used to get the microservice description for subtenant-mgmt microservice. + * @returns string + */ private getMsDescription(): string { const currentTenant = this.appState.currentTenant.value; return `Microservice that allows tenant ${currentTenant.name} to get access to this tenant.`; } + /** + * sha256 is used to hash a string with sha256. + * @param message + * @returns + */ private async sha256(message: string): Promise { // encode as UTF-8 const msgBuffer = new TextEncoder().encode(message); @@ -279,6 +316,11 @@ export class FakeMicroserviceService implements OnDestroy { return hashHex; } + /** + * GetBootstrapUser is used to get the bootstrap user for all subtenants. + * @param app + * @returns + */ private async getBootstrapUser(app: IApplication) { const bootstrapCredentialsEndpoint = `/application/applications/${app.id}/bootstrapUser`; const res = await this.fetchClient.fetch(bootstrapCredentialsEndpoint); @@ -286,10 +328,13 @@ export class FakeMicroserviceService implements OnDestroy { return { tenant, password, user: name } as ICredentials; } - private async getMicroserviceSubscriptions( - bootstrapCredentials: ICredentials, - baseUrl?: string - ): Promise { + /** + * GetMicroserviceSubscriptions is used to get the microservice subscriptions for all subtenant's bootstrap user. + * @param bootstrapCredentials + * @param baseUrl + * @returns + */ + private async getMicroserviceSubscriptions(bootstrapCredentials: ICredentials): Promise { const loginMode = get(this.loginService, 'loginMode.type', 'BASIC'); const client: Client = new Client(new BasicAuth(bootstrapCredentials)); if (loginMode !== 'BASIC') { @@ -298,13 +343,6 @@ export class FakeMicroserviceService implements OnDestroy { ); throw Error(`OAuth on the management/enterprise tenant is currently not supported.`); } - // unable to set use BearerAuth together with OAuthCookie, as Cookie is always preferred.. - // if (loginMode === 'BASIC') { - // client = new Client(new BasicAuth(bootstrapCredentials)); - // } else { - // const token = await this.getAccessToken(bootstrapCredentials); - // client = new Client(new BearerAuth({ token })); - // } const microserviceSubscriptionsEndpoint = '/application/currentApplication/subscriptions'; const res = await client.core.fetch(microserviceSubscriptionsEndpoint); @@ -321,6 +359,10 @@ export class FakeMicroserviceService implements OnDestroy { }); } + /** + * CreateDummyMicroserviceIfNotExisting creates a dummy microservice if it does not exist. + * @returns + */ private async createDummyMicroserviceIfNotExisting() { let app = await this.findDummyMicroservice(); if (!app) { @@ -336,6 +378,10 @@ export class FakeMicroserviceService implements OnDestroy { return app; } + /** + * findDummyMicroservice finds the dummy microservice in the tenant. + * @returns + */ private async findDummyMicroservice() { const ms = await this.getDummyMicroserviceObjForCreation(); const { data: appList } = await this.appService.listByName(ms.name); @@ -345,6 +391,10 @@ export class FakeMicroserviceService implements OnDestroy { return null; } + /** + * getDummyMicroserviceObjForCreation creates a dummy microservice object. + * @returns + */ private async getDummyMicroserviceObjForCreation(): Promise> { const msKey = await this.getMsKey(); const msName = await this.getMsName(); @@ -357,11 +407,19 @@ export class FakeMicroserviceService implements OnDestroy { }; } + /** + * createDummyMicroservice creates a dummy microservice. + * @returns + */ private async createDummyMicroservice() { const ms = await this.getDummyMicroserviceObjForCreation(); return this.appService.create(ms); } + /** + * updateDummyMicroserviceRoles updates the dummy microservice roles. + * @param appId + */ private async updateDummyMicroserviceRoles(appId: string | number) { const msWithoutType = omit(await this.getDummyMicroserviceObjForCreation(), ['type']); return this.appService.update({ @@ -370,6 +428,11 @@ export class FakeMicroserviceService implements OnDestroy { }); } + /** + * deleteApp deletes the given application. + * @param app + * @returns + */ private deleteApp(app: IApplication) { return this.appService.delete(app.id); } diff --git a/src/services/provisioning.service.ts b/src/services/provisioning.service.ts index 799896b..b770355 100644 --- a/src/services/provisioning.service.ts +++ b/src/services/provisioning.service.ts @@ -7,12 +7,14 @@ import { IManagedObject, InventoryBinaryService, InventoryService, + IOperationBulk, IResult, IRole, ITenantOption, IUserGroup, UserGroupService } from '@c8y/client'; +import { IOperation } from '@modules/provisioning/firmware-provisioning/models/operation.model'; import { cloneDeep } from 'lodash-es'; import { v4 as uuidv4 } from 'uuid'; @@ -52,65 +54,14 @@ export class ProvisioningService { return copy; } - async provisionLegacyFirmwareToTenants(clients: Client[], firmware: IManagedObject): Promise { - const url = (firmware.url as string) || ''; - if (url && url.includes('/inventory/binaries/')) { - const binaryMOId = this.binaryService.getIdFromUrl(url); - const { data: binaryMO } = await this.inventoryService.detail(binaryMOId); - const binaryResponse = (await this.binaryService.download(binaryMOId)) as Response; - const binary = await binaryResponse.blob(); - return await Promise.all( - clients.map((tmp) => this.provisionLegacyFirmwareToTenant(tmp, firmware, binaryMO, binary)) - ); - } else { - return await Promise.all(clients.map((tmp) => this.provisionLegacyFirmwareToTenant(tmp, firmware))); - } - } - - async provisionLegacyFirmwareToTenant( - client: Client, - firmware: IManagedObject, - binaryMO?: IManagedObject, - binary?: Blob - ): Promise { - const knownFirmware = await this.checkIfFirmwareIsAvailable(client, firmware); - if (!knownFirmware) { - const newFirmwareMO = this.createCopyOfManagedObject(firmware); - newFirmwareMO[this.provisioningIdent].firmwareMOId = firmware.id; - if (binaryMO && binary) { - const newBinaryMO = await client.inventoryBinary.create(binary, this.createCopyOfManagedObject(binaryMO)); - newFirmwareMO.url = newBinaryMO.data.self; - } - const res = await client.inventory.create(newFirmwareMO); - return res.data; - } - return null; - } - async checkIfFirmwareIsAvailable(client: Client, firmware: IManagedObject): Promise { const filter = { - query: `$filter=((type eq 'c8y_Firmware') and (name eq '${firmware.name}') and (version eq '${firmware.version}') and has(${this.provisioningIdent}) and (${this.provisioningIdent}.firmwareMOId eq '${firmware.id}'))` + query: `$filter=((type eq 'c8y_Firmware') and (name eq '${firmware.name}') and has(${this.provisioningIdent}) and (${this.provisioningIdent}.firmwareMOId eq '${firmware.id}'))` }; const { data: firmwares } = await client.inventory.list(filter); return firmwares.length > 0 ? firmwares[0] : null; } - async deprovisionLegacyFirmwareFromTenants(clients: Client[], firmware: IManagedObject): Promise { - return await Promise.all(clients.map((tmp) => this.deprovisionLegacyFirmwareFromTenant(tmp, firmware))); - } - - async deprovisionLegacyFirmwareFromTenant(client: Client, firmware: IManagedObject): Promise { - const findFirmware = await this.checkIfFirmwareIsAvailable(client, firmware); - if (findFirmware) { - const url = findFirmware.url as string; - if (url && url.includes('/inventory/binaries/')) { - const binaryMOId = this.binaryService.getIdFromUrl(url); - await client.inventoryBinary.delete(binaryMOId); - } - await client.inventory.delete(findFirmware.id); - } - } - async provisionSmartRESTTemplates(clients: Client[], templateIds: string[]): Promise { const promArray = templateIds.map((tmp) => this.provisionSmartRESTTemplate(clients, tmp)); await Promise.all(promArray); @@ -183,6 +134,11 @@ export class ProvisioningService { return Promise.all(promArray); } + deprovisionTenantOptionToTenants(clients: Client[], tenantOption: ITenantOption): Promise[]> { + const promArray = clients.map((client) => client.options.tenant.delete(tenantOption)); + return Promise.all(promArray); + } + async removeUserGroupFromTenants(clients: Client[], userGroup: IUserGroup): Promise { if (userGroup && userGroup.customProperties && userGroup.customProperties.uuid) { const uuid: string = userGroup.customProperties.uuid; @@ -378,4 +334,270 @@ export class ProvisioningService { await client.inventory.delete(foundGroup); } } + + /** + * ScheduleBulkFirmwareUpgrade schedules a bulk firmware upgrade on the given client + * @param client + * @param operationDetails + * @param groupId + * @param firmware + * @param firmwareVersion + * @returns the output of the bulk operation + */ + async scheduleBulkFirmwareUpgrade( + client: Client, + operationDetails: IOperation, + firmware: IManagedObject, + firmwareVersion: IManagedObject + ): Promise { + // Create a dynamic group based on the firmware type + const groupId = await this.createDynamicGroup(client, firmware.c8y_Filter.type); + + // If no group ID is returned, exit the function + if (!groupId) { + return; + } + + // Define the bulk operation details + const bulkOperation: IOperationBulk = { + groupId: groupId, + creationRamp: operationDetails.delay, + startDate: operationDetails.date.toISOString(), + note: operationDetails.description, + operationPrototype: { + c8y_Firmware: { + name: firmware.name, + version: firmwareVersion.c8y_Firmware.version, + url: firmwareVersion.c8y_Firmware.url + }, + description: firmware.description, + inet_OperationType: 'AUTOMATIC_FIRMWARE_UPGRADE' + } + }; + + // Create the bulk operation and get the output + const { data: operationOutput } = await client.operationBulk.create(bulkOperation); + + return operationOutput; + } + + /** + * CheckIfAutomaticFirmwareUpgradeEnabled checks if automatic firmware upgrade is enabled for the given device query + * @param client + * @param deviceQuery + * @returns boolean + */ + async checkIfAutomaticFirmwareUpgradeEnabled(client: Client, deviceQuery: string): Promise { + const filter = { + query: deviceQuery + }; + const { data: devices } = await client.inventory.list(filter); + return devices.length > 0; + } + /** + * GetAutomaticFirmwareUpgradeGroups returns all groups with automatic firmware upgrade enabled + * @param client + * @returns group IDs + */ + async getAutomaticFirmwareUpgradeGroups(client: Client): Promise { + const query = { + __filter: { + type: 'c8y_DeviceGroup', + inet_AutomaticFirmwareUpgrade: true + } + }; + const filter = { + currentPage: 1, + pageSize: 2000 + }; + const { data: groups } = await client.inventory.listQuery(query, filter); + return groups.length > 0 ? groups.map((group) => group.id) : []; + } + + /** + * CreateDynamicGroup creates a dynamic group on the given client + * @param client + * @returns the ID of the dynamic group + */ + async createDynamicGroup(client: Client, deviceType: string): Promise { + // Retrieve the IDs of all device groups that are enabled for automatic firmware upgrades + const groupIds = await this.getAutomaticFirmwareUpgradeGroups(client); + + // Construct the device query string based on the device type and whether there are any group IDs + const deviceQuery = + groupIds.length > 0 + ? `$filter=((type eq '${deviceType}') and ((inet_AutomaticFirmwareUpgrade eq true) or (bygroupid(${groupIds.join()}))))` + : `$filter=((type eq '${deviceType}') and (inet_AutomaticFirmwareUpgrade eq true))`; + + // Check if automatic firmware upgrade is enabled for the device query + const automaticFirmwareUpgradeEnabled = await this.checkIfAutomaticFirmwareUpgradeEnabled(client, deviceQuery); + + // If automatic firmware upgrade is not enabled, return an empty id + if (!automaticFirmwareUpgradeEnabled) { + return ''; + } + + // Define the dynamic group object + const dynamicGroup = { + name: 'Bulk Firmware Upgrade Group', + type: 'c8y_DynamicGroup', + c8y_IsDynamicGroup: { invisible: {} }, + c8y_DeviceQueryString: deviceQuery + }; + + // Create the dynamic group and get the output + const { data: dynamicGroupMO } = await client.inventory.create(dynamicGroup); + + // Return the ID of the dynamic group + return dynamicGroupMO.id; + } + + /** + * ProvisionLegacyFirmwareToTenant is used to create firmware and firmware versions for the given client + * @param client + * @param firmware + * @param selectedVersion + * @param binary + * @param binaryMO + * @returns + */ + async provisionLegacyFirmwareToTenant( + client: Client, + firmware: IManagedObject, + selectedVersion: IManagedObject, + operationDetails: IOperation + ): Promise { + let isNewFirmware = false; + // Check if firmware is already available if not available create it + const knownFirmware = await this.checkIfFirmwareIsAvailable(client, firmware).then(async (res) => { + if (!res) { + // Create a new firmware managed object if the firmware is not available. + const newFirmwareMO = this.createCopyOfManagedObject(firmware); + newFirmwareMO[this.provisioningIdent].firmwareMOId = firmware.id; + const res = await client.inventory.create(newFirmwareMO); + isNewFirmware = true; + return res.data; + } else { + // Update the firmware managed object if the firmware is available. + const updateFirmwareMO = this.createCopyOfManagedObject(firmware); + updateFirmwareMO[this.provisioningIdent].firmwareMOId = firmware.id; + updateFirmwareMO.id = res.id; + const updateResponse = await client.inventory.update(updateFirmwareMO); + return updateResponse.data; + } + }); + + let knownFirmwareVersion; + + if (isNewFirmware) { + // If the firmware is new, create a new firmware version managed object. + const newFirmwareVersionMO = this.createCopyOfManagedObject(selectedVersion); + const res = await client.inventory.childAdditionsCreate(newFirmwareVersionMO, knownFirmware.id); + knownFirmwareVersion = res.data; + } else { + // If the firmware is not new, check if the firmware version is available. + knownFirmwareVersion = await this.checkIfFirmwareVersionIsAvailable( + client, + knownFirmware, + selectedVersion.c8y_Firmware.version + ); + + if (!knownFirmwareVersion) { + // If the firmware version is not available, create a new firmware version managed object. + const newFirmwareVersionMO = this.createCopyOfManagedObject(selectedVersion); + const res = await client.inventory.childAdditionsCreate(newFirmwareVersionMO, knownFirmware.id); + knownFirmwareVersion = res.data; + } + } + + // create scheduled automatic bulk operation for automatic firmware enabled devices and groups + await this.scheduleBulkFirmwareUpgrade(client, operationDetails, firmware, selectedVersion); + + return knownFirmwareVersion; + } + + /** + * CheckIfFirmwareVersionIsAvailable checks if the given firmware version is already available on the given client + * @param client + * @param firmware + * @param version + * @returns + */ + async checkIfFirmwareVersionIsAvailable( + client: Client, + firmware: IManagedObject, + version: string + ): Promise { + const query = { + __filter: { + __bygroupid: firmware.id, + type: 'c8y_FirmwareBinary', + 'c8y_Firmware.version': version + } + }; + const { data: firmwares } = await client.inventory.listQuery(query); + return firmwares.length > 0 ? firmwares[0] : null; + } + /** + * ProvisionLegacyFirmwareToTenants is used to create firmware and firmware versions for the given clients + * @param clients + * @param firmware + * @param selectedVersion + * @returns + */ + async provisionLegacyFirmwareToTenants( + clients: Client[], + firmware: IManagedObject, + selectedVersion: IManagedObject, + operationDetails: IOperation + ): Promise { + return await Promise.all( + clients.map((tmp) => this.provisionLegacyFirmwareToTenant(tmp, firmware, selectedVersion, operationDetails)) + ); + } + + /** + * DeProvisionFirmware is used to remove firmware and firmware versions from the given clients + * @param clients + * @param firmware + * @param selectedVersion + * @returns + */ + async deprovisionLegacyFirmwareFromTenants( + clients: Client[], + firmware: IManagedObject, + selectedVersion: IManagedObject + ): Promise { + return await Promise.all( + clients.map((tmp) => this.deprovisionLegacyFirmwareFromTenant(tmp, firmware, selectedVersion)) + ); + } + + /** + * DeProvisionFirmware is used to remove firmware and firmware versions from the given client + * @param client + * @param firmware + * @param selectedVersion + */ + async deprovisionLegacyFirmwareFromTenant( + client: Client, + firmware: IManagedObject, + selectedVersion: IManagedObject + ): Promise { + const findFirmware = await this.checkIfFirmwareIsAvailable(client, firmware); + if (findFirmware) { + const findFirmwareVersion = await this.checkIfFirmwareVersionIsAvailable( + client, + findFirmware, + selectedVersion.c8y_Firmware.version + ); + if (findFirmwareVersion) { + await client.inventory.delete(findFirmwareVersion.id); + } + const { data } = await client.inventory.detail(findFirmware.id); + if (data.childAdditions.references.length === 0) { + await client.inventory.delete(findFirmware.id); + } + } + } } From eb6ec70f185cc88b67a940bdaf7bc5ed70018907 Mon Sep 17 00:00:00 2001 From: "EUR\\DIKR" Date: Fri, 2 Aug 2024 10:25:01 +0530 Subject: [PATCH 2/2] reverting package.json changes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ab2045b..b2ba6f6 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.7.2", "description": "Tool for managing subtenants from a c8y management or enterprise tenant", "scripts": { - "start": "c8ycli server -u https://isc.na-dev.inet.com/", + "start": "c8ycli server", "build": "rimraf dist && c8ycli build", "build:ci": "rimraf dist && c8ycli build --ci true ./", "deploy": "c8ycli deploy",