UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1,173 lines (1,168 loc) 112 kB
import * as i1 from '@c8y/ngx-components'; import { BaseColumn, gettext, getBasicInputArrayFormFieldConfig, CommonModule, memoize, C8yTranslatePipe, CoreModule, TypeaheadComponent, ModalSelectionMode, PRODUCT_EXPERIENCE_EVENT_SOURCE, toObservable, FormsModule as FormsModule$1, OperationRealtimeService } from '@c8y/ngx-components'; import * as i0 from '@angular/core'; import { Component, Injectable, HostListener, ViewChild, ChangeDetectionStrategy, Input, EventEmitter, forwardRef, Output, NgModule } from '@angular/core'; import * as i3 from '@angular/router'; import { RouterModule } from '@angular/router'; import { DeviceGridModule } from '@c8y/ngx-components/device-grid'; import { get, head, isNil, set, assign, isUndefined, isString, cloneDeep, map as map$1, omitBy, pick, remove, forEach, find, property, uniqBy, has, isEmpty, isEqual } from 'lodash-es'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; import * as i4 from '@angular/common'; import { CommonModule as CommonModule$1 } from '@angular/common'; import { __decorate, __metadata } from 'tslib'; import * as i1$1 from '@c8y/client'; import { QueriesUtil, OperationStatus } from '@c8y/client'; import { of, from, defer, throwError, merge, NEVER, BehaviorSubject, pipe, Observable, Subject, interval } from 'rxjs'; import { map, take, switchMap, withLatestFrom, filter, takeWhile, tap, debounceTime, shareReplay, mergeMap, debounce } from 'rxjs/operators'; import * as i5 from '@angular/forms'; import { FormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms'; import { saveAs } from 'file-saver'; import * as i2 from '@ngx-translate/core'; import { PopoverModule } from 'ngx-bootstrap/popover'; class DescriptionGridColumn extends BaseColumn { constructor(initialColumnConfig) { super(initialColumnConfig); this.name = 'description'; this.path = 'description'; this.header = gettext('Description'); this.filterable = true; this.filteringConfig = { fields: getBasicInputArrayFormFieldConfig({ key: 'descriptions', label: initialColumnConfig?.filterLabel ?? gettext('Filter items by description'), addText: gettext('Add next`description`'), tooltip: gettext('Use * as a wildcard character'), placeholder: initialColumnConfig?.placeholder ?? gettext('Description…') }), getFilter(model) { const filter = {}; if (model.descriptions.length) { filter.description = { __in: model.descriptions }; } return filter; } }; this.sortable = true; this.sortingConfig = { pathSortingConfigs: [{ path: this.path }] }; } } class DeviceTypeCellRendererComponent { constructor(context) { this.context = context; } ngOnInit() { this.deviceType = get(this.context?.item, this.context?.property?.path); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DeviceTypeCellRendererComponent, deps: [{ token: i1.CellRendererContext }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: DeviceTypeCellRendererComponent, isStandalone: true, selector: "c8y-device-type-cell-renderer", ngImport: i0, template: "<span *ngIf=\"deviceType; else emptyText\">\n {{ deviceType }}\n</span>\n<ng-template #emptyText>\n <small class=\"text-muted\">\n <em translate>Undefined`device type`</em>\n </small>\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: DeviceGridModule }, { kind: "ngmodule", type: TooltipModule }, { kind: "ngmodule", type: RouterModule }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DeviceTypeCellRendererComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-device-type-cell-renderer', standalone: true, imports: [CommonModule, DeviceGridModule, TooltipModule, RouterModule], template: "<span *ngIf=\"deviceType; else emptyText\">\n {{ deviceType }}\n</span>\n<ng-template #emptyText>\n <small class=\"text-muted\">\n <em translate>Undefined`device type`</em>\n </small>\n</ng-template>\n" }] }], ctorParameters: () => [{ type: i1.CellRendererContext }] }); class DeviceTypeGridColumn extends BaseColumn { constructor(initialColumnConfig) { super(initialColumnConfig); this.name = 'deviceType'; this.path = initialColumnConfig?.path ?? 'c8y_Filter.type'; this.header = gettext('Device type'); this.cellRendererComponent = DeviceTypeCellRendererComponent; this.filterable = true; this.filteringConfig = { fields: [ ...getBasicInputArrayFormFieldConfig({ key: 'types', label: initialColumnConfig?.filterLabel ?? gettext('Filter items by device type'), addText: gettext('Add next`type`'), tooltip: gettext('Use * as a wildcard character'), placeholder: initialColumnConfig?.placeholder ?? 'c8y_Linux', optional: true }), { key: 'noDeviceType', type: 'switch', templateOptions: { label: gettext('No device type') } } ], getFilter(model) { const filter = { __or: {} }; if (model.types?.length) { filter.__or = { 'c8y_Filter.type': { __in: model.types } }; } if (model.noDeviceType) { filter.__or = { ...filter.__or, __or: { __not: { __has: 'c8y_Filter.type' }, 'c8y_Filter.type': '' } }; } return filter; } }; this.sortable = true; this.sortingConfig = { pathSortingConfigs: [{ path: this.path }] }; } } var RepositoryType; (function (RepositoryType) { RepositoryType["FIRMWARE"] = "c8y_Firmware"; RepositoryType["SOFTWARE"] = "c8y_Software"; RepositoryType["CONFIGURATION"] = "c8y_ConfigurationDump"; RepositoryType["PROFILE"] = "c8y_Profile"; })(RepositoryType || (RepositoryType = {})); const REPOSITORY_BINARY_TYPES = { [RepositoryType.SOFTWARE]: 'c8y_SoftwareBinary', [RepositoryType.FIRMWARE]: 'c8y_FirmwareBinary', [RepositoryType.CONFIGURATION]: 'c8y_ConfigurationDumpBinary' }; var DeviceConfigurationOperation; (function (DeviceConfigurationOperation) { DeviceConfigurationOperation["UPLOAD_CONFIG"] = "c8y_UploadConfigFile"; DeviceConfigurationOperation["DOWNLOAD_CONFIG"] = "c8y_DownloadConfigFile"; DeviceConfigurationOperation["CONFIG"] = "c8y_Configuration"; DeviceConfigurationOperation["SEND_CONFIG"] = "c8y_SendConfiguration"; })(DeviceConfigurationOperation || (DeviceConfigurationOperation = {})); const PRODUCT_EXPERIENCE_REPOSITORY_SHARED = { SOFTWARE: { EVENTS: { REPOSITORY: 'softwareRepository', DEVICE_TAB: 'deviceSoftware' }, COMPONENTS: { ADD_SOFTWARE_MODAL: 'add-software-modal', DEVICE_SOFTWARE_CHANGES: 'device-software-changes', DEVICE_SOFTWARE_LIST: 'device-software-list' }, ACTIONS: { APPLY_SOFTWARE_CHANGES: 'applySoftwareChanges', CLEAR_SOFTWARE_CHANGES: 'clearSoftwareChanges', OPEN_INSTALL_SOFTWARE: 'openInstallSoftwareModal', OPEN_UPDATE_SOFTWARE: 'openUpdateSoftwareModal', DELETE_SOFTWARE: 'deleteSoftware' }, RESULTS: { ADD_SOFTWARE: 'addSoftware', ADD_SOFTWARE_VERSION: 'addSoftwareVersion', EDIT_SOFTWARE: 'editSoftware' } }, FIRMWARE: { EVENTS: { REPOSITORY: 'firmwareRepository', DEVICE_TAB: 'deviceFirmware' }, COMPONENTS: { ADD_FIRMWARE_MODAL: 'add-firmware-modal', ADD_FIRMWAR_PATCH_MODAL: 'add-firmware-patch-modal', FIRMWARE_DEVICE_TAB: 'firmware-device-tab', DEVICE_FIRMWARE_LIST: 'device-firmware-list' }, ACTIONS: { OPEN_INSTALL_FIRMWARE_DIALOG: 'openInstallFirmwareDialog', OPEN_REPLACE_FIRMWARE_DIALOG: 'openReplaceFirmwareDialog', OPEN_INSTALL_FIRMWARE_PATCH_DIALOG: 'openInstallFirmwarePatchDialog' }, RESULTS: { ADD_FIRMWARE: 'addFirmware', ADD_FIRMWARE_VERSION: 'addFirmwareVersion', ADD_FIRMWARE_PATCH: 'addFirmwarePatch', EDIT_FIRMWARE: 'editFirmware', CREATE_FIRMWARE_UPDATE_OPERATION: 'createFirmwareUpdateOperation' } }, SHARED: { COMPONENTS: { REPOSITORY_SELECT_MODAL: 'repository-select-modal', SELECT_CONFIGURATION_MODAL: 'select-configuration-modal' } } }; class RepositoryService { constructor(inventory, inventoryBinary, operation, alert, event, operationRealtime, eventBinary, serviceRegistry, globalConfigService) { this.inventory = inventory; this.inventoryBinary = inventoryBinary; this.operation = operation; this.alert = alert; this.event = event; this.operationRealtime = operationRealtime; this.eventBinary = eventBinary; this.serviceRegistry = serviceRegistry; this.globalConfigService = globalConfigService; this.dateFrom = new Date(0); this.dateTo = new Date(Date.now() + 86400000); // 1 day in the future this.queriesUtil = new QueriesUtil(); this.advancedSoftwareService = head(this.serviceRegistry.get('asm')); } /** * Lists repository entries of given type. * @param type The type of repository entries to list. * @param options Extra listing options. */ listRepositoryEntries(type, options) { const defaultOrder = [{ name: 1 }]; const defaultFilters = { type }; const legacyFilters = { __has: `url` }; let filters = {}; let fullQuery = (options && options.query) || {}; if (!options || (options && !options.skipDefaultOrder)) { fullQuery = this.queriesUtil.addOrderbys(fullQuery, defaultOrder, 'prepend'); } fullQuery = this.queriesUtil.addAndFilter(fullQuery, defaultFilters); if (options && options.partialTextFilter) { const { partialText, properties } = options.partialTextFilter; const orFilter = { __or: properties.map(property => ({ [property]: `*${partialText}*` })) }; fullQuery = this.queriesUtil.addAndFilter(fullQuery, orFilter); } if (options && options.partialName) { // backwards compatibility if fullQuery = this.queriesUtil.addAndFilter(fullQuery, { name: `*${options.partialName}*` }); } if (options && options.skipLegacy) { fullQuery = this.queriesUtil.addAndFilter(fullQuery, { __not: legacyFilters }); } filters = { query: this.queriesUtil.buildQuery(fullQuery), pageSize: 50, withTotalPages: true, ...((options && options.params) || {}) }; return this.inventory.list(filters); } async create(modal, type, mo = {}) { switch (type) { case RepositoryType.FIRMWARE: case RepositoryType.SOFTWARE: return this.createRepositoryObject(modal, type); case RepositoryType.CONFIGURATION: Object.assign(modal, { selected: { id: mo.id, name: modal.version }, configurationType: modal.configurationType, name: modal.version }); if (!modal.deviceType && mo.id) { modal.deviceType = null; } if (!modal.selected && mo.id) { modal.configurationType = null; } const repositoryObject = this.createRepositoryObject(modal, type); if (mo.url) { const newBinaryUrl = (await repositoryObject).url; this.removeOutdatedBinary(newBinaryUrl, mo.url); } return repositoryObject; } } async createRepositoryObject(modal, type) { let binary; let binaryURL; let repositoryEntry; let repositoryBinary; const mos = []; const { selected: { id: selectedId }, binary: { file, url } } = modal; try { const globalParam = await this.getGlobalFragment(type); if (file) { ({ data: binary } = await this.saveBinary(file, globalParam)); ({ self: binaryURL } = binary); if (type === RepositoryType.CONFIGURATION) { modal.binary.url = binaryURL; } mos.push(binary); } else { binaryURL = url; } ({ data: repositoryEntry } = await this.createOrUpdateRepositoryEntry({ ...modal, ...globalParam }, type)); if (isNil(selectedId)) { mos.push(repositoryEntry); } if (type !== RepositoryType.CONFIGURATION) { ({ data: repositoryBinary } = await this.createRepositoryBinary({ ...modal, ...globalParam }, binaryURL, type, repositoryEntry)); mos.push(repositoryBinary); } if (file) { await this.linkBinary(repositoryBinary, binary, repositoryEntry); } return repositoryEntry; } catch (error) { this.cleanUp(mos); this.errorMsg(); // Propagate error throw error; } } saveBinary(file, global) { return this.inventoryBinary.create(file, global); } async createOrUpdateRepositoryEntry(modal, type) { const { selected: { id, name }, description, deviceType, c8y_Global, binary } = modal; const mo = { id, name, description, type, c8y_Global }; if (deviceType && type !== RepositoryType.CONFIGURATION) { set(mo, 'c8y_Filter.type', deviceType); } if ((deviceType || id) && type === RepositoryType.CONFIGURATION) { set(mo, 'deviceType', deviceType); } if (modal.softwareType) { set(mo, 'softwareType', modal.softwareType.softwareType); } if ((modal.configurationType || id) && type === RepositoryType.CONFIGURATION) { set(mo, 'configurationType', modal.configurationType); } if (type === RepositoryType.CONFIGURATION) { set(mo, 'url', binary?.url); } return id ? this.inventory.update(mo) : this.inventory.create(mo); } createRepositoryBinary(modal, binaryURL, type, parent) { const mo = this.prepareRepositoryBinaryMO(modal, binaryURL, type); return this.inventory.childAdditionsCreate(mo, parent); } prepareRepositoryBinaryMO(modal, binaryURL, type) { const { version, patchVersion, dependency, c8y_Global } = modal; const result = { type: REPOSITORY_BINARY_TYPES[type], [type]: { url: binaryURL }, c8y_Global }; if (dependency) { set(result, [type, 'version'], patchVersion); assign(result, { c8y_Patch: { dependency: dependency.c8y_Firmware.version } }); } else { set(result, [type, 'version'], version); } return result; } async linkBinary(repositoryBinary, binary, repositoryEntry) { if (repositoryBinary) { const { id: repositoryBinaryId } = repositoryBinary; if (binary) { const { id: binaryId } = binary; return this.inventory.childAdditionsAdd(binaryId, repositoryBinaryId); } } else return this.inventory.childAdditionsAdd(binary, repositoryEntry); } cleanUp(mosToDelete) { mosToDelete.forEach(mo => { const { c8y_IsBinary } = mo; isUndefined(c8y_IsBinary) ? this.delete(mo) : this.inventoryBinary.delete(mo); }); } delete(entity) { return this.inventory.delete(entity, { forceCascade: true }); } errorMsg() { const msg = gettext('Failed to save'); this.alert.danger(msg); } getBaseVersionsCount$(entry) { if (this.isLegacyEntry(entry)) { return of(1); } return from(this.listBaseVersions(entry, { withTotalElements: true })).pipe(map(({ paging }) => paging.totalElements)); } getBaseVersionFromMO(mo) { return this.isPatch(mo) ? get(mo, 'c8y_Patch.dependency') : get(mo, 'c8y_Firmware.version'); } isPatch(mo) { return !!get(mo, 'c8y_Patch.dependency'); } getPatchVersionsCount$(entry, baseVersion) { if (this.isLegacyEntry(baseVersion)) { return of(0); } return from(this.listPatchVersions(entry, baseVersion, { withTotalElements: true })).pipe(map(({ paging }) => paging.totalElements)); } isLegacyEntry(entry) { return Boolean(entry.url); } /** * Lists all versions (base and patch ones) of given top level entry. * Versions are ordered by creation time (assuming the earlier created, the older the version). * @param entry Top level repository entry. * @param params Additional query params. */ listAllVersions(entry, params = {}) { if (this.isLegacyEntry(entry)) { return this.getBaseVersionResultListForLegacyEntry(entry); } const VERSION_FILTER_ORDER = { __filter: {}, __orderby: [{ 'creationTime.date': -1 }] }; return this.listChildren(entry, VERSION_FILTER_ORDER, params); } /** * Lists base versions of given top level entry. * Versions are ordered by creation time (assuming the earlier created, the older the version). * @param entry Top level repository entry. * @param params Additional query params. */ listBaseVersions(entry, params = {}) { if (this.isLegacyEntry(entry)) { return this.getBaseVersionResultListForLegacyEntry(entry); } const NO_PATCH_FILTER_ORDER = { __filter: { __not: { __has: 'c8y_Patch' } }, __orderby: [{ 'creationTime.date': -1 }] }; return this.listChildren(entry, NO_PATCH_FILTER_ORDER, params); } /** * Lists patch versions of given base version under the entry. * Versions are ordered by creation time (assuming the earlier created, the older the version). * @param entry Top level repository entry. * @param baseVersion Base version. * @param params Additional query params. */ listPatchVersions(entry, baseVersion, params = {}) { const version = isString(baseVersion) ? baseVersion : get(baseVersion, 'c8y_Firmware.version'); const PATCH_FILTER_ORDER = { __filter: { 'c8y_Patch.dependency': version }, __orderby: [{ 'creationTime.date': -1 }] }; return this.listChildren(entry, PATCH_FILTER_ORDER, params); } /** * Lists patch versions of given base version under the entry including the base version. * Versions are ordered by creation time (assuming the earlier created, the older the version). * In terms of legacy base version the entry gets transformed to fit the needed data model. * @param entry Top level repository entry. * @param baseVersion Base version. * @param params Additional query params. */ listBaseVersionAndPatches(entry, baseVersion, params = {}) { if (this.isLegacyEntry(entry)) { return Promise.resolve({ data: [ Object.assign({ c8y_Firmware: { version: entry.version, url: entry.url } }, entry) ] }); } const PATCH_FILTER_ORDER = { __filter: { __or: { 'c8y_Patch.dependency': baseVersion.c8y_Firmware.version, 'c8y_Firmware.version': baseVersion.c8y_Firmware.version } }, __orderby: [{ 'c8y_Patch.dependency': 1 }, { 'c8y_Firmware.version': 1 }] }; return this.listChildren(entry, PATCH_FILTER_ORDER, params); } listChildren(entry, filters = {}, params = {}) { const childrenFilters = { __bygroupid: entry.id }; const query = this.queriesUtil.addAndFilter(filters, childrenFilters); // FIXME: needed because of issue in forOf directive (...) params.withTotalPages = true; return this.inventory.listQuery(query, params); } /** * Fetches all items from the list starting with the provided page. * @param firstPage The first page of the list to fetch all items for. */ async fetchAllItemsFromList(firstPage) { let allItems; if (!firstPage.then) { allItems = [...firstPage]; } else { let { paging, data: items } = await firstPage; allItems = [...items]; while (paging && paging.nextPage) { ({ paging, data: items } = await paging.next()); allItems = [...allItems, ...items]; } } return allItems; } /** * Gets top level repository entry managed object for base or patch version. * @param mo Base or patch version managed object with parents. */ getRepositoryEntryMO$(mo) { if (!mo) { return of(undefined); } const [reference] = get(mo, 'additionParents.references'); const id = get(reference, 'managedObject.id'); return id ? from(this.inventory.detail(id, { withChildren: false })).pipe(map(({ data }) => data)) : of(undefined); } /** * Gets base or patch version managed object. * @param deviceRepositoryFragment Device repository fragment. * @param type Top level repository entry type. * @param configuration Configuration object with options: * - **skipLegacy** - `boolean` - Exclude legacy entries. * - **filters** - `object` - Filter object. * * @deprecated as it doesn't support 'missing url' case */ getRepositoryBinaryMoByVersion(deviceRepositoryFragment, type, { skipLegacy = false, filters = {} } = {}) { const { version, url, name } = deviceRepositoryFragment; const repositoryBinaryType = REPOSITORY_BINARY_TYPES[type]; let query; const newModelBaseVersionQuery = { [`${type}.version`]: version, [`${type}.url`]: url, type: repositoryBinaryType }; const legacyVersionQuery = { url, type, name }; filters = { withChildren: false, withParents: true, ...filters }; if (skipLegacy) { query = { __and: { ...newModelBaseVersionQuery } }; } else { query = { __or: [{ __and: { ...newModelBaseVersionQuery } }, { __and: { ...legacyVersionQuery } }] }; } return this.inventory.listQuery(query, filters).then(({ data }) => head(data)); } getBinaryName$(binaryUrl) { if (!binaryUrl) { return of('---'); } const binaryId = this.inventoryBinary.getIdFromUrl(binaryUrl); if (!binaryId) { return of(binaryUrl); } return defer(() => this.inventory.detail(binaryId).then(result => result.data)).pipe(map(mo => mo.name)); } /** * Generates an inventory query object which can be used to find * repository entries of specified type matching the type of provided device. * @param repositoryType The type of repository entries which will be queried with the generated query. * @param device The device for which matching repository entries will be queried with the generated query. */ getDeviceTypeQuery(repositoryType, device) { let result = { type: repositoryType }; if (repositoryType === RepositoryType.CONFIGURATION) { if (device.type) { result = this.queriesUtil.addAndFilter(result, { __or: [{ deviceType: device.type }, { __not: { __has: `deviceType` } }] }); } } else { result = this.queriesUtil.addAndFilter(result, { __or: [ { 'c8y_Filter.type': device.type }, { 'c8y_Filter.type': '' }, { __not: { __has: `c8y_Filter.type` } } ] }); } return result; } /** * Generates an inventory query object which can be used to find * repository entries matching the predefined software types provided in the device. * @param device The device for which matching repository entries will be queried with the generated query. * @param query The query to which the software types filters will be attached. Default value is an object containg repository type software. */ getSoftwareTypeQuery(device, query) { let result = { ...(query || {}), type: RepositoryType.SOFTWARE }; if (device.c8y_SupportedSoftwareTypes) { result = this.queriesUtil.addAndFilter(result, { __or: [device.c8y_SupportedSoftwareTypes.map(type => ({ softwareType: type }))] }); } return result; } /** * Generates an inventory query object which can be used to find configuration repository entries * matching the type of provided device and specified configuration type. * @param device The device for which matching repository entries will be queried with the generated query. * @param configurationType Configuration type for which matching repository entries will be queried with the generated query. */ getConfigurationTypeQuery(device, configurationType) { const query = this.getDeviceTypeQuery(RepositoryType.CONFIGURATION, device); return this.queriesUtil.addAndFilter(query, { __or: [ { configurationType }, { configurationType: '' }, { __not: { __has: `configurationType` } } ] }); } /** * Gets the list of software installed in the device in the uniform format. * Supports c8y_SoftwareList and c8y_Software fragments. * @param device The device whose software list should be returned. */ getDeviceSoftwareList(device) { if (device.c8y_SoftwareList) { return cloneDeep(device.c8y_SoftwareList); } if (device.c8y_Software) { return map$1(device.c8y_Software, (version, name) => ({ name, version })); } return []; } /** * Prepares a software update operation for given device and the list of changes, and sends it to the device. * @param device The device which the operation should be prepared for and sent to. * @param changes The list of software changes which should be applied. */ async createSoftwareUpdateOperation(device, changes) { const operation = await this.getSoftwareUpdateOperation(device, changes); return (await this.operation.create(operation)).data; } /** * Prepares a software update operation for given device and changes. * Returned operation type depends on device's supported operations. * Supports c8y_SoftwareUpdate, c8y_SoftwareList, and c8y_Software operations. * @param device The device for which operation should be prepared. * @param changes The list of software changes which should be applied. */ async getSoftwareUpdateOperation(device, changes) { const operation = { deviceId: device.id, description: `Apply software changes: ${changes .map(change => `${change.action} "${change.name}"${change.version ? ` (version: ${change.version})` : ''}`) .join(', ')}` }; if (device.c8y_SupportedOperations.includes('c8y_SoftwareUpdate')) { operation.c8y_SoftwareUpdate = (cloneDeep(changes) || []).map(change => omitBy(change, isNil)); } else if (device.c8y_SupportedOperations.includes('c8y_SoftwareList')) { operation.c8y_SoftwareList = cloneDeep(await this.getCurrentSoftware(device, 'c8y_SoftwareList', [])); changes.forEach(change => { const deviceSoftware = pick(omitBy(change, isNil), [ 'name', 'version', 'url', 'softwareType' ]); if (change.action === 'delete') { remove(operation.c8y_SoftwareList, deviceSoftware); } if (change.action === 'install') { const softwareItemToUpdateIdx = operation.c8y_SoftwareList.findIndex(item => item.name === change.name); if (softwareItemToUpdateIdx > -1) { // update software operation.c8y_SoftwareList.splice(softwareItemToUpdateIdx, 1, deviceSoftware); } else { // install software operation.c8y_SoftwareList.push(deviceSoftware); } } }); } else if (device.c8y_SupportedOperations.includes('c8y_Software')) { operation.c8y_Software = cloneDeep(await this.getCurrentSoftware(device, 'c8y_Software', {})); changes.forEach(change => { if (change.action === 'delete') { delete operation.c8y_Software[change.name]; } if (change.action === 'install') { operation.c8y_Software[change.name] = change.version; } }); } return operation; } /** * Extracts the list of device software changes from given operation in the context of given device. * @param operation The operation from which the list should be extracted. * @param device The target device of the operation. */ async getDeviceSoftwareChangesFromOperation(operation, device) { if (operation.c8y_SoftwareUpdate) { return cloneDeep(operation.c8y_SoftwareUpdate); } if (operation.c8y_SoftwareList) { return await this.getDeviceSoftwareChangesFromSoftwareListOperation(operation, device); } if (operation.c8y_Software) { return await this.getDeviceSoftwareChangesFromSoftwareOperation(operation, device); } return []; } /** * Prepares a firmware update operation for given device and the selected repository binary, and sends it to the device. * @param device The device which the operation should be prepared for and sent to. * @param selectedOption The selected repository binary option. */ async createFirmwareUpdateOperation(device, selectedOption) { const operation = this.getFirmwareUpdateOperation(device, selectedOption); return (await this.operation.create(operation)).data; } /** * Prepares a firmware update operation for given device and selected version. * Supports c8y_Firmware operation. * @param device The device for which operation should be prepared. * @param selectedOption Selected firmware version. */ getFirmwareUpdateOperation(device, selectedOption) { delete selectedOption.id; const operation = { deviceId: device.id, description: `Update firmware to: "${selectedOption.name}"${selectedOption.version ? ` (version: ${selectedOption.version})` : ''}`, c8y_Firmware: { ...selectedOption } }; return operation; } /** * Prepares a configuration file upload operation for given device and configuration type. * @param device The device for which operation should be prepared. * @param configurationType Selected configuration type. * @param isLegacy A legacy operation is created without a configurationType. */ getUploadConfigurationFileOperation(device, configurationType, isLegacy = false) { if (isLegacy) { return { deviceId: device.id, description: `Retrieve configuration snapshot from device ${device.name}`, c8y_UploadConfigFile: {} }; } return { deviceId: device.id, description: `Retrieve ${configurationType} configuration snapshot from device ${device.name}`, c8y_UploadConfigFile: { type: configurationType } }; } /** * Prepares a configuration file download operation for given device and configuration type. * @param device The device for which operation should be prepared. * @param configurationType Selected configuration type. * @param binaryUrl The url of a binary to be downloaded. * @param isLegacy A legacy operation is created without a configurationType. */ getDownloadConfigurationFileOperation(device, configurationType, configSnapshot, isLegacy = false) { if (isLegacy) { return { deviceId: device.id, description: `Send configuration snapshot ${configSnapshot.name} to device ${device.name}`, c8y_DownloadConfigFile: { url: configSnapshot.binaryUrl, c8y_ConfigurationDump: { id: configSnapshot.id } } }; } return { deviceId: device.id, description: `Send configuration snapshot ${configSnapshot.name} of configuration type ${configurationType} to device ${device.name}`, c8y_DownloadConfigFile: { url: configSnapshot.binaryUrl, type: configurationType } }; } /** * Gets the last firmware update operation for given device. * Looks for c8y_Firmware operations. * @param deviceId The ID of the device to find an operation for. */ async getLastFirmwareUpdateOperation(deviceId) { const filters = { deviceId, dateFrom: new Date(0).toISOString(), dateTo: new Date(Date.now()).toISOString(), revert: true, pageSize: 1 }; return this.getFirstMatchingOperation([{ ...filters, fragmentType: 'c8y_Firmware' }]); } /** * Gets the last software update operation for given device. * Looks for c8y_SoftwareUpdate, c8y_SoftwareList, or c8y_Software operations. * @param deviceId The ID of the device to find an operation for. */ async getLastSoftwareUpdateOperation(deviceId) { const filters = { deviceId, dateFrom: new Date(0).toISOString(), dateTo: new Date(Date.now()).toISOString(), revert: true, pageSize: 1 }; return this.getLatestMatchingOperation([ { ...filters, fragmentType: 'c8y_SoftwareUpdate' }, { ...filters, fragmentType: 'c8y_SoftwareList' }, { ...filters, fragmentType: 'c8y_Software' } ]); } /** * Iterates over the list of filters and queries the operations. * If a query returns at least one operation, the first one will be returned. * Otherwise the next query will be performed. * If none of the queries returns any operation, null will be returned. * @param filtersList The list of filters for the queries. */ async getFirstMatchingOperation(filtersList) { let matchingOperation = null; for (const filters of filtersList) { const operations = (await this.operation.list(filters)).data; if (operations.length) { matchingOperation = operations[0]; break; } } return matchingOperation; } /** * Iterates over the list of filters and queries the operations. * It compares the operations retrieved by the queries by 'creationTime' * and return the latest one. * If none of the queries returns any operation, null will be returned. * @param filtersList The list of filters for the queries. */ async getLatestMatchingOperation(filtersList) { let matchingOperation = null; for (const filters of filtersList) { const operations = (await this.operation.list(filters)).data; if (operations.length) { if (matchingOperation) { matchingOperation = new Date(matchingOperation.creationTime).getTime() < new Date(operations[0].creationTime).getTime() ? operations[0] : matchingOperation; } else { matchingOperation = operations[0]; } } } return matchingOperation; } /** * Creates the operation and returns an observable to track its progress. * Fails the observable when the operation returns FAILED status. * Completes the observable when the operation returns SUCCESSFUL status. * @param operation The operation to create and track. */ createObservedOperation(operation) { return from(this.operation.create(operation)).pipe(map(({ data }) => data), take(1), switchMap(createdOperation => this.observeOperation(createdOperation))); } /** * Returns an observable to track progress of given operation. * Fails the observable when the operation returns FAILED status. * Completes the observable when the operation returns SUCCESSFUL status. * @param operation The operation to be observed. */ observeOperation(operation) { const observedOperation$ = of(operation); const operationUpdates$ = observedOperation$.pipe(switchMap(observedOperation => this.operationRealtime.onAll$(observedOperation.deviceId)), map(({ data }) => data), withLatestFrom(observedOperation$), filter(([operationUpdate, observedOperation]) => operationUpdate.id === observedOperation.id), switchMap(([operationUpdate]) => { if (operationUpdate.status === OperationStatus.FAILED) { return throwError(operationUpdate); } return of(operationUpdate); }), takeWhile(operationUpdate => operationUpdate.status !== OperationStatus.SUCCESSFUL, true)); return merge(observedOperation$, operationUpdates$); } /** * Gets a single event with latest creationTime for the given device Id and event type. * @param deviceId The device Id for which the events should be queried. * @param type Event type. */ async getLatestConfigurationEvent(deviceId, type) { const eventFilter = { source: deviceId, type, dateFrom: this.dateFrom.toISOString(), dateTo: this.dateTo.toISOString(), pageSize: 1 }; const { data } = await this.event.list(eventFilter); return data[0]; } /** * Gets a list of operations for the given device Id, and operation type. * @param deviceId The device Id for which the operation should be queried. * @param operationType Operation type fragment. */ async getConfigFileOperationList(deviceId, operationType) { const operationFilter = { deviceId, fragmentType: operationType, dateFrom: this.dateFrom.toISOString(), dateTo: this.dateTo.toISOString(), revert: true, pageSize: 2000 }; return (await this.operation.list(operationFilter)).data; } /** * Gets latest uploaded configuration snapshot for the given device, and configuration type. * @param device The device for which the configuration snapshot was uploaded. * @param configurationType Selected configuration type. */ async getConfigSnapshot(device, configurationType) { const event = await this.getLatestConfigurationEvent(device.id, configurationType); let configSnapshot; if (event) { configSnapshot = { time: event.time, name: event.text, deviceType: device.type, configurationType }; try { configSnapshot.binary = await (await this.eventBinary.download(event)).text(); if (event.c8y_IsBinary) { configSnapshot.binaryType = event.c8y_IsBinary.type; } } catch (ex) { const msg = gettext('Could not get the binary.'); this.alert.danger(msg); } } return configSnapshot; } async getLegacyConfigSnapshot(deviceId) { let configSnapshot; let mo; const device = (await this.inventory.detail(deviceId, { withChildren: false })).data; const snapshotId = device.c8y_ConfigurationDump && device.c8y_ConfigurationDump.id; if (!snapshotId) { return; } try { mo = (await this.inventory.detail(snapshotId)).data; } catch (ex) { // do nothing } if (mo) { configSnapshot = { time: mo.creationTime, name: mo.name }; configSnapshot.binary = await this.getBinaryText(mo.url, { allowExternal: false }); } return configSnapshot; } /** * Returns a binary object as text. * @param binaryUrl The URL to find binary * @param options The object with additional options: * - **allowExternal** - `boolean` - allows downloading external binary file * - **noAlerts** - `boolean` - do not display an alert message; defaults to `false` */ async getBinaryText(binaryUrl, options) { const binaryId = this.inventoryBinary.getIdFromUrl(binaryUrl); let res; if (!binaryId) { if (options.allowExternal) { res = await this.getExternalBinaryResponse(binaryUrl, options); } } else { res = await this.getInternalBinaryResponse(binaryId, options); } if (!res) { return null; } return res.text(); } /** * Returns a binary object as File. * @param binaryUrl The URL to find binary * @param options The object with additional options: * - **allowExternal** - `boolean` - allows downloading external binary file */ async getBinaryFile(binaryUrl, options) { const binaryId = this.inventoryBinary.getIdFromUrl(binaryUrl); if (!binaryId && !options.allowExternal) { return null; } // @TODO: note that it doesn't solve issue with external binary here, such url won't have binaryId, so we won't know the name or contentType to use in File constructor, let's add a @FIXME comment for now? const { name, contentType } = (await this.inventory.detail(binaryId)).data; const res = !!binaryId ? await this.getInternalBinaryResponse(binaryId) : await this.getExternalBinaryResponse(binaryUrl); const arrayBuffer = await res.arrayBuffer(); return new File([arrayBuffer], name, { type: contentType }); } /** * Gets the last configuration update operation for given device. * Looks for c8y_Configuration and c8y_SendConfiguration operations. * @param deviceId The ID of the device to find an operation for. */ async getLastConfigUpdateOperation(deviceId) { const filters = { deviceId, dateFrom: new Date(0).toISOString(), dateTo: new Date(Date.now()).toISOString(), revert: true, pageSize: 1 }; return this.getLatestMatchingOperation([ { ...filters, fragmentType: 'c8y_Configuration' }, { ...filters, fragmentType: 'c8y_SendConfiguration' } ]); } /** * Prepares a configuration download operation for given device and its current configuration. * Supports c8y_SendConfiguration operation. * @param device The device for which operation should be prepared. */ createTextBasedConfigurationReloadOperation(device) { return { deviceId: device.id, description: gettext('Requested current configuration'), c8y_SendConfiguration: {} }; } /** * Prepares a configuration update operation for the given device. * Supports c8y_Configuration operation. * @param device The device for which operation should be prepared. * @param config The configuration which will update the existing one. */ createTextBasedConfigurationUpdateOperation(device, config) { return { deviceId: device.id, description: gettext('Configuration update'), c8y_Configuration: { config } }; } async getBinary(binaryId) { try { return await this.inventoryBinary.download(binaryId); } catch (ex) { const msg = gettext('Could not get the binary.'); this.alert.danger(msg); } } /** * Gets all available snapshots from the repository for the given device. * @param device The device for which the snapshots should be prepared. * @param configurationType Selected configuration type. */ async getSnapshotsFromRepository(device, configurationType) { const searchQuery = this.getConfigurationTypeQuery(device, configurationType); const res = await this.listRepositoryEntries(RepositoryType.CONFIGURATION, { query: searchQuery, params: { pageSize: 100 } }); return res.data; } /** * Checks if a device already have a given software installed * @param deviceId Id of the device to be checked * @param software The software to be checked */ async isSoftwareInstalledOnDevice(deviceId, software) { const isASMAvailable = await this.advancedSoftwareService?.isASMAvailable(); if (!isASMAvailable) { return false; } const queryFilter = { deviceId }; if (software?.name) { set(queryFilter, 'name', software.name); } if (software?.version) { set(queryFilter, 'version', software.version); } return this.advancedSoftwareService.list(queryFilter).then(result => !!result.data?.length); } /** * Returns a binary object. * @param binaryId binary ID * @param options The object with additional options: * - **noAlerts** - `boolean` - do not display an alert message; defaults to `false` */ async getInternalBinaryResponse(binaryId, options = {}) { let res; try { res = await this.inventoryBinary.download(binaryId); } catch (ex) { if (!options.noAlerts) { const msg = gettext('Could not get the binary.'); this.alert.danger(msg); } } return res; } /** * Returns a binary object. * @param binaryUrl The URL to find binary * @param options The object with additional options: * - **noAlerts** - `boolean` - do not display an alert message; defaults to `false` */ async getExternalBinaryResponse(binaryUrl, options = {}) { let res; try { const fetchRes = await fetch(binaryUrl); if (fetchRes.status >= 400) { throw res; } res = fetchRes; } catch { if (!options.noAlerts) { const msg = gettext('Could not get the external binary'); this.alert.danger(msg); } } return res; } getBaseVersionResultListForLegacyEntry(entry) { return Promise.resolve({ res: {}, data: [ { ...entry, [entry.t