+
+
+
+
+
+
+
+
+
+
+ Full registration
+
+
+ Creates all device credentials and devices using provided list of property values.
+ Devices can start communicating with the platform immediately.
+
+
+
+
+
+
+
+
+
+
+
+
+
0">
+
+
+
+
+
+
+
+ {{log?.deviceId}}
+
+
+
+
+ {{log?.failureReason}}
+
+
+
+
+
+
+
+
+
+
\ 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
+
+
\ 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
+
+
\ 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 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0"
+ (click)="textFilter$.next('')"
+ px-event="Clear filtering firmware"
+ >
+
+
+
+
+
+
+
+
+
+
+
+ No firmwares to display.
+
+
+
+ 0
+ "
+>
+
+
No results to display.
+
Refine your search terms or check your spelling.
+
+
+ 0"
+ [ngClass]="{ 'dd-low': (firmwares$ | async)?.data.length < 10 }"
+>
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ "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: `
`,
+})
+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
+
+
+
+