@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1,173 lines (1,168 loc) • 112 kB
JavaScript
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