UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

602 lines (597 loc) 144 kB
import * as i0 from '@angular/core'; import { Injectable, EventEmitter, forwardRef, Component, Input, inject, DestroyRef, NgModule } from '@angular/core'; import { gettext } from '@c8y/ngx-components/gettext'; import * as i2 from '@c8y/ngx-components'; import { NavigatorNode, SelectModalComponent, ProductExperienceDirective, PRODUCT_EXPERIENCE_EVENT_SOURCE, ModalSelectionMode, TitleComponent, BreadcrumbComponent, BreadcrumbItemComponent, FormGroupComponent, C8yTranslateDirective, RequiredInputPlaceholderDirective, IconDirective, ListGroupComponent, ListItemComponent, ListItemIconComponent, ListItemBodyComponent, EmptyStateComponent, C8yTranslatePipe, BuiltInActionType, Status, ActionBarItemComponent, HelpComponent, DataGridComponent, EmptyStateContextDirective, ColumnDirective, MessagesComponent, MessageDirective, RealtimeButtonComponent, TypeaheadComponent, ForOfDirective, HighlightComponent, ManagedObjectRealtimeService, ViewContext, CoreModule, CommonModule, hookNavigator, hookRoute } from '@c8y/ngx-components'; import * as i1 from '@c8y/client'; import { QueriesUtil, OperationStatus } from '@c8y/client'; import { get, isEmpty, mapValues, omitBy, keyBy, uniqBy, keys, pickBy, sortBy, toArray, isNil, assign, concat, uniqWith, isEqual, has, cloneDeep } from 'lodash-es'; import * as i1$1 from '@c8y/ngx-components/repository/shared'; import { PRODUCT_EXPERIENCE_REPOSITORY_SHARED, RepositoryType, RepositorySelectModalComponent, RepositoryItemNameGridColumn, DeviceTypeGridColumn, SharedRepositoryModule } from '@c8y/ngx-components/repository/shared'; import { Subject, from, BehaviorSubject, pipe, combineLatest } from 'rxjs'; import { switchMap, map, take, distinctUntilChanged, shareReplay, filter, tap } from 'rxjs/operators'; import { AsyncPipe, NgIf, NgFor, NgTemplateOutlet, NgClass } from '@angular/common'; import * as i5 from '@angular/router'; import { RouterModule } from '@angular/router'; import * as i2$1 from 'ngx-bootstrap/dropdown'; import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; import { PopoverDirective, PopoverModule } from 'ngx-bootstrap/popover'; import { TooltipDirective, TooltipModule } from 'ngx-bootstrap/tooltip'; import { ButtonsModule } from 'ngx-bootstrap/buttons'; import * as i7 from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import * as i4 from 'ngx-bootstrap/modal'; import * as i4$1 from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { OperationDetailsComponent, OperationDetailsModule } from '@c8y/ngx-components/operations/operation-details'; class DeviceProfileNavigationFactory { async get() { if (!this.nodeItem) { this.nodeItem = new NavigatorNode({ label: gettext('Device profiles'), path: '/device-profiles', icon: 'c8y-device-profile', parent: gettext('Management') }); } return this.nodeItem; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DeviceProfileNavigationFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DeviceProfileNavigationFactory }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DeviceProfileNavigationFactory, decorators: [{ type: Injectable }] }); class DeviceProfileService { constructor(inventoryService, operationService, alertService) { this.inventoryService = inventoryService; this.operationService = operationService; this.alertService = alertService; this.dateFrom = new Date(0); this.dateTo = new Date(Date.now() + 86400000); // 1 day in the future this.NOT_INSTALLED_WARNING = gettext('Not installed on the device'); this.VERSION_MISSMATCH_WARNING = gettext('Version mismatch'); this.SAME_URL_WARNING = gettext('Installed configuration has the same URL but different name or type than the one in the profile'); this.queriesUtil = new QueriesUtil(); } createDeviceProfile(deviceProfile) { if (get(deviceProfile, 'c8y_Filter.type') === '') { delete deviceProfile.c8y_Filter.type; } return this.inventoryService.create(deviceProfile); } /** * Determines the available device profiles for a given device by considering device type * and the supported software types declared by the devices. Because of limitations in the * Inventory Query API the methods return profile that contain at least one of the supported * software types and omits profiles having only non-supported software types. Resulting device * profiles need to be further filtered on the client side to exclude the ones that contain * non-supported software types next to the supported ones. * * @param device A device MO * @param name Optional device profile name filter * @returns Candidate device profiles that contain at least on software with supported type. */ getDeviceProfilesByDevice(device, name = null) { const deviceTypeFilter = { __or: [ { 'c8y_Filter.type': device.type }, { 'c8y_Filter.type': '' }, { __not: { __has: 'c8y_Filter.type' } } ] }; let softwareTypeFilter = {}; if (!isEmpty(device.c8y_SupportedSoftwareTypes)) { softwareTypeFilter = { __hasany: device.c8y_SupportedSoftwareTypes.map((type) => `softwareType!${type}`) }; } let query = this.queriesUtil.addAndFilter(deviceTypeFilter, softwareTypeFilter); let profileNameFilter = {}; if (!isEmpty(name)) { profileNameFilter = { name: `*${name}*` }; } query = this.queriesUtil.addAndFilter(query, profileNameFilter); return this.getDeviceProfiles(query); } /** * @deprecated Use `getDeviceProfilesByDevice` instead as it also considers the supported software types. */ getDeviceProfilesByDeviceType(deviceType) { const deviceTypeFilter = { __or: [ { 'c8y_Filter.type': deviceType }, { 'c8y_Filter.type': '' }, { __not: { __has: 'c8y_Filter.type' } } ] }; return this.getDeviceProfiles(deviceTypeFilter); } getDeviceProfiles(andQuery) { let query = { type: 'c8y_Profile' }; const filter = { pageSize: 100, withTotalPages: true }; query = this.queriesUtil.addAndFilter(query, andQuery || {}); return this.inventoryService.listQuery(query, filter); } async getProfileOperation(deviceId) { const filter = { deviceId, fragmentType: 'c8y_DeviceProfile', dateFrom: this.dateFrom.toISOString(), dateTo: this.dateTo.toISOString(), revert: true, pageSize: 1 }; const operation = (await this.operationService.list(filter)).data[0]; return operation && operation.status !== OperationStatus.SUCCESSFUL ? operation : undefined; } async createProfileOperation(device, deviceProfile) { let operation; const operationCfg = { deviceId: device.id, profileId: deviceProfile.id, profileName: deviceProfile.name, c8y_DeviceProfile: deviceProfile.c8y_DeviceProfile, description: `Assign device profile ${deviceProfile.name} to device ${device.name}` }; try { const { data } = await this.operationService.create(operationCfg); operation = data; } catch (ex) { this.alertService.addServerFailure(ex); } return operation; } getFirmwareItems(device, selectedProfile) { const deviceFirmware = device.c8y_Firmware; const profileFirmware = get(selectedProfile, 'c8y_DeviceProfile.firmware'); const deviceItems = []; const profileItems = []; if (deviceFirmware) { deviceItems.push(deviceFirmware); } if (profileFirmware) { profileItems.push(profileFirmware); } return this.createProfileComparison(deviceItems, profileItems, 'name', 'version', null, this.getAlert('firmware')); } getSoftwareItems(device, selectedProfile) { const deviceSoftware = device.c8y_SoftwareList; const profileSoftware = get(selectedProfile, 'c8y_DeviceProfile.software'); return this.createProfileComparison(deviceSoftware, profileSoftware, 'name', 'version', 'softwareType', this.getAlert('software')); } getConfigurationItems(device, selectedProfile) { const deviceConfiguration = []; Object.keys(device).forEach(key => { if (key.slice(0, 18) === 'c8y_Configuration_') { deviceConfiguration.push(device[key]); } }); const profileConfiguration = get(selectedProfile, 'c8y_DeviceProfile.configuration'); return this.createProfileComparison(deviceConfiguration, profileConfiguration, 'url', null, 'type', this.getAlert('configuration')); } /** * Aligns device profile managed object's `softwareType!*` fragments to the software items * included in the device profile. Removes obsolete software type fragments and adds new. * * @param profilePartial The device profile managed object which `softwareType!*` fragments will be adjusted. * @returns The adjusted device profile managed object. */ alignSoftwareTypeFragments(profilePartial, profile) { if (!profilePartial?.c8y_DeviceProfile?.software || !profile) { return profilePartial; } const removedSoftwareTypes = mapValues(omitBy(profile, (_, key) => typeof key === 'string' && !key.startsWith('softwareType!')), () => null); const softwareTypesToAdd = mapValues(keyBy(uniqBy(profilePartial.c8y_DeviceProfile.software, 'softwareType'), (profile) => `softwareType!${profile.softwareType}`), () => ({})); const result = { ...profilePartial, ...removedSoftwareTypes, ...softwareTypesToAdd }; return result; } getSoftwareTypes(profile) { return keys(pickBy(profile, (_, key) => typeof key === 'string' && key.startsWith('softwareType!'))).map(softwareType => softwareType.substr('softwareType!'.length)); } getAlert(itemType) { const notInstalled = (comparisionResult) => { return !comparisionResult.device ? this.NOT_INSTALLED_WARNING : ''; }; switch (itemType) { case 'firmware': case 'software': return (comparisionResult) => { return comparisionResult.device && comparisionResult.profile && comparisionResult.device.itemDetails !== comparisionResult.profile.itemDetails ? this.VERSION_MISSMATCH_WARNING : notInstalled(comparisionResult); }; case 'configuration': return (comparisionResult) => { return comparisionResult.device && comparisionResult.profile && (comparisionResult.device.itemName !== comparisionResult.profile.itemName || comparisionResult.device.itemDetails !== comparisionResult.profile.itemDetails) ? this.SAME_URL_WARNING : notInstalled(comparisionResult); }; default: return notInstalled; } } createProfileComparison(deviceItems = [], profileItems = [], mergeByProperty, propertyNameWithDetails, propertyNameWithType, getAlert) { const comparisonObj = this.createProfileComparisonFromDeviceItems(deviceItems, mergeByProperty, propertyNameWithDetails, propertyNameWithType); const extendedComparisonObj = this.extendProfileComparisonWithProfileItems(comparisonObj, profileItems, mergeByProperty, propertyNameWithDetails, propertyNameWithType, getAlert); return sortBy(toArray(extendedComparisonObj), 'name'); } createProfileComparisonFromDeviceItems(deviceItems, mergeByProperty, propertyNameWithDetails, propertyNameWithType) { return deviceItems.reduce((comapritionItem, deviceItem) => Object.assign(comapritionItem, { [deviceItem[mergeByProperty]]: { device: omitBy({ itemName: deviceItem.name, itemDetails: deviceItem[propertyNameWithDetails], itemType: deviceItem[propertyNameWithType], itemUrl: deviceItem.url }, isNil), profile: undefined } }), {}); } extendProfileComparisonWithProfileItems(comparisonObj, profileItems, mergeByProperty, propertyNameWithDetails, propertyNameWithType, getAlert) { profileItems.forEach(profileItem => { const comparisionResult = { profile: omitBy({ itemName: profileItem.name, itemDetails: profileItem[propertyNameWithDetails], itemType: profileItem[propertyNameWithType], itemUrl: profileItem.url }, isNil), device: comparisonObj[profileItem[mergeByProperty]] ? comparisonObj[profileItem[mergeByProperty]].device : undefined }; comparisionResult.comparisonAlert = getAlert(comparisionResult); comparisonObj[profileItem[mergeByProperty]] = comparisionResult; }); return comparisonObj; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DeviceProfileService, deps: [{ token: i1.InventoryService }, { token: i1.OperationService }, { token: i2.AlertService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DeviceProfileService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DeviceProfileService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i1.InventoryService }, { type: i1.OperationService }, { type: i2.AlertService }] }); class SelectConfigurationModalComponent { constructor(repositoryService) { this.repositoryService = repositoryService; this.PRODUCT_EXPERIENCE = PRODUCT_EXPERIENCE_REPOSITORY_SHARED; this.title = gettext('Select configuration'); this.load = new Subject(); this.configurations = this.load.pipe(switchMap(() => this.getItems()), map(({ data }) => this.aggregate(data))); this.resultEmitter = new EventEmitter(); this.deviceTypeQuery = {}; this.searchQuery = {}; this.labels = { ok: gettext('Save') }; this.queriesUtil = new QueriesUtil(); } search(searchTerm) { if (!searchTerm) { this.searchQuery = {}; } else { this.searchQuery = this.queriesUtil.addOrFilter({ name: `*${searchTerm}*` }, { configurationType: `*${searchTerm}*` }); } this.load.next(); } result(selectedItems) { this.resultEmitter.emit(selectedItems); } getItems() { return this.repositoryService.listRepositoryEntries(RepositoryType.CONFIGURATION, { query: this.queriesUtil.addOrFilter(this.deviceTypeQuery, this.searchQuery), params: { pageSize: 100 } }); } aggregate(mos) { const selectedItems = this.selected; return mos.reduce((acc, curr) => { curr.configurationType = curr.configurationType || curr.name; const selected = selectedItems && selectedItems.filter(val => val.url === curr.url).length > 0; const selectModalOption = { body: [{ value: curr.name }], obj: curr, selected }; let selectModalObject = acc.find(val => val.body[0].value === curr.configurationType); if (selectModalObject) { selectModalObject.options.push(selectModalOption); } else { selectModalObject = { groupId: curr.id, body: [{ value: curr.configurationType }], options: [selectModalOption] }; acc.push(selectModalObject); } return acc; }, []); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SelectConfigurationModalComponent, deps: [{ token: i1$1.RepositoryService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: SelectConfigurationModalComponent, isStandalone: true, selector: "c8y-select-configuration-modal", providers: [ { provide: PRODUCT_EXPERIENCE_EVENT_SOURCE, useExisting: forwardRef(() => SelectConfigurationModalComponent) } ], ngImport: i0, template: "<c8y-select-modal\n [icon]=\"'gears'\"\n [title]=\"title\"\n [items]=\"configurations | async\"\n [mode]=\"'multi'\"\n (result)=\"result($event)\"\n (search)=\"search($event)\"\n [disableSelected]=\"true\"\n [labels]=\"labels\"\n c8yProductExperience\n inherit\n suppressDataOverriding\n [actionData]=\"{ component: PRODUCT_EXPERIENCE.SHARED.COMPONENTS.SELECT_CONFIGURATION_MODAL }\"\n></c8y-select-modal>\n", dependencies: [{ kind: "component", type: SelectModalComponent, selector: "c8y-select-modal", inputs: ["icon", "title", "subTitle", "items", "mode", "disableSelected", "showFilter", "additionalFilterTemplate", "areMoreEntries", "labels", "noItemsMessage", "hideEmptyItems"], outputs: ["result", "search", "onChoiceUpdated"] }, { kind: "directive", type: ProductExperienceDirective, selector: "[c8yProductExperience]", inputs: ["actionName", "actionData", "inherit", "suppressDataOverriding"] }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SelectConfigurationModalComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-select-configuration-modal', providers: [ { provide: PRODUCT_EXPERIENCE_EVENT_SOURCE, useExisting: forwardRef(() => SelectConfigurationModalComponent) } ], imports: [SelectModalComponent, ProductExperienceDirective, AsyncPipe], template: "<c8y-select-modal\n [icon]=\"'gears'\"\n [title]=\"title\"\n [items]=\"configurations | async\"\n [mode]=\"'multi'\"\n (result)=\"result($event)\"\n (search)=\"search($event)\"\n [disableSelected]=\"true\"\n [labels]=\"labels\"\n c8yProductExperience\n inherit\n suppressDataOverriding\n [actionData]=\"{ component: PRODUCT_EXPERIENCE.SHARED.COMPONENTS.SELECT_CONFIGURATION_MODAL }\"\n></c8y-select-modal>\n" }] }], ctorParameters: () => [{ type: i1$1.RepositoryService }] }); var DeviceProfileOperation; (function (DeviceProfileOperation) { DeviceProfileOperation["APPLY_PROFILE"] = "c8y_DeviceProfile"; })(DeviceProfileOperation || (DeviceProfileOperation = {})); const PRODUCT_EXPERIENCE_DEVICE_PROFILE = { EVENTS: { REPOSITORY: 'deviceProfileRepository', DEVICE_TAB: 'deviceProfileTab' }, COMPONENTS: { DEVICE_PROFILE_LIST: 'device-profile-list', ADD_DEVICE_PROFILE: 'add-device-profile', DEVICE_PROFILE: 'device-profile', DEVICE_TAB_PROFILE: 'device-tab-profile' }, ACTIONS: { CANCEL: 'cancel', CREATE: 'create', REMOVE: 'remove', ADD: 'add', SAVE: 'save', ASSIGN_DEVICE_PROFILE: 'assignDeviceProfile' }, RESULTS: { ADD_SOFTWARE: 'addSoftware' }, FRAGMENTS: { FIRMWARE: 'firmware', SOFTWARE: 'software', CONFGIURATION: 'configuration' } }; class DeviceProfileComponent { constructor(route, alertService, inventoryService, bsModal, repositoryService, deviceProfileService) { this.route = route; this.alertService = alertService; this.inventoryService = inventoryService; this.bsModal = bsModal; this.repositoryService = repositoryService; this.deviceProfileService = deviceProfileService; this.DEVICE_TYPE_POPOVER = gettext('The device profile will be available for assignments on devices of the specified type. Otherwise, it will be available for all device types.'); this.DEVICE_TYPE_DISABLED_POPOVER = gettext('Device type cannot be changed on profiles with already defined firmware, software or configuration since they may not be applicable to devices of the new device type.'); this.PRODUCT_EXPERIENCE = PRODUCT_EXPERIENCE_DEVICE_PROFILE; this.productExperienceEvent = { eventName: PRODUCT_EXPERIENCE_DEVICE_PROFILE.EVENTS.REPOSITORY, data: { component: PRODUCT_EXPERIENCE_DEVICE_PROFILE.COMPONENTS.DEVICE_PROFILE } }; this.queriesUtil = new QueriesUtil(); } async ngOnInit() { const profileId = this.route.snapshot.paramMap.get('id'); this.deviceProfile = (await this.getDeviceProfile(profileId)); if (this.deviceProfile) { this.profileName = this.deviceProfile.name; if (!this.deviceProfile.c8y_DeviceProfile.software) { this.deviceProfile.c8y_DeviceProfile.software = []; } if (!this.deviceProfile.c8y_DeviceProfile.configuration) { this.deviceProfile.c8y_DeviceProfile.configuration = []; } } } addFirmware() { const initialState = { deviceTypeQuery: this.getDeviceTypeQuery(RepositoryType.FIRMWARE), repositoryType: RepositoryType.FIRMWARE, repositoryEntriesWithVersionsFn$: modalDialog => this.getRepositoryEntriesWithVersions$(modalDialog.content.searchTerm, RepositoryType.FIRMWARE), icon: 'c8y-firmware', title: gettext('Select firmware'), mode: ModalSelectionMode.SINGLE, productExperienceEvent: this.productExperienceEvent }; const modal = this.bsModal.show(RepositorySelectModalComponent, { ignoreBackdropClick: true, keyboard: false, ariaLabelledBy: 'modal-title', initialState }); if (initialState.repositoryEntriesWithVersionsFn$) { modal.content.repositoryEntriesWithVersions$ = initialState.repositoryEntriesWithVersionsFn$(modal); } modal.content.load.next(); modal.content.resultEmitter.pipe(take(1)).subscribe(firmwareList => { const [firmware] = firmwareList; if (!firmware) { return; } const deviceProfilePartial = { c8y_DeviceProfile: this.deviceProfile.c8y_DeviceProfile || {} }; assign(deviceProfilePartial.c8y_DeviceProfile, { firmware: { name: firmware.name, version: firmware.version, url: firmware.url, isPatch: firmware.isPatch, patchDependency: firmware.patchDependency } }); this.updateDeviceProfile(deviceProfilePartial); }); } getRepositoryEntriesWithVersions$(searchTerm$, repoType) { return searchTerm$.pipe(distinctUntilChanged(), switchMap(searchTerm => this.repositoryService.listRepositoryEntries(repoType, { query: this.getDeviceTypeQuery(repoType), partialName: searchTerm.name, params: { pageSize: 100 }, skipLegacy: true })), map(({ data }) => data), map(mos => this.getAndAssignRepositoryBinaries(mos)), shareReplay(1)); } getAndAssignRepositoryBinaries(mos) { mos.forEach(mo => { mo.versions = this.repositoryService.listBaseVersions(mo); }); return mos; } addConfiguration() { const modal = this.bsModal.show(SelectConfigurationModalComponent, { ignoreBackdropClick: true, keyboard: false, ariaLabelledBy: 'modal-title', initialState: { productExperienceEvent: this.productExperienceEvent } }); modal.content.deviceTypeQuery = this.getDeviceTypeQuery(RepositoryType.CONFIGURATION); modal.content.selected = this.deviceProfile.c8y_DeviceProfile.configuration; modal.content.load.next(); modal.content.resultEmitter.pipe(take(1)).subscribe(selectedConfigurations => { const selectedMapped = selectedConfigurations.map(selectedItem => { return assign({ url: selectedItem.url, name: selectedItem.name }, selectedItem.configurationType ? { type: selectedItem.configurationType } : {}); }); const merged = concat(selectedMapped, this.deviceProfile.c8y_DeviceProfile.configuration || []); const configuration = uniqWith(merged, (arrVal, othVal) => { return arrVal.type && othVal.type && arrVal.type === othVal.type; }); const deviceProfilePartial = { c8y_DeviceProfile: this.deviceProfile.c8y_DeviceProfile || {} }; assign(deviceProfilePartial.c8y_DeviceProfile, { configuration }); this.updateDeviceProfile(deviceProfilePartial); }); } addSoftware() { const initialState = { deviceTypeQuery: this.getDeviceTypeQuery(RepositoryType.SOFTWARE), repositoryType: RepositoryType.SOFTWARE, repositoryEntriesWithVersionsFn$: modalDialog => this.getRepositoryEntriesWithVersions$(modalDialog.content.searchTerm, RepositoryType.SOFTWARE), selected: this.deviceProfile.c8y_DeviceProfile.software, icon: 'c8y-tools', title: gettext('Select software'), mode: ModalSelectionMode.MULTI, productExperienceEvent: this.productExperienceEvent }; const modal = this.bsModal.show(RepositorySelectModalComponent, { ignoreBackdropClick: true, keyboard: false, ariaLabelledBy: 'modal-title', initialState }); if (initialState.repositoryEntriesWithVersionsFn$) { modal.content.repositoryEntriesWithVersions$ = initialState.repositoryEntriesWithVersionsFn$(modal); } modal.content.load.next(); modal.content.resultEmitter.pipe(take(1)).subscribe(selectedSoftware => { const selectedMapped = selectedSoftware.map(selectedItem => { return { name: selectedItem.name, version: selectedItem.version, url: selectedItem.url, softwareType: selectedItem.softwareType, action: 'install' }; }); const merged = concat(selectedMapped, this.deviceProfile.c8y_DeviceProfile.software || []); const software = uniqWith(merged, (arrVal, othVal) => { return arrVal.name && othVal.name && arrVal.name === othVal.name; }); const deviceProfilePartial = { c8y_DeviceProfile: this.deviceProfile.c8y_DeviceProfile || {} }; assign(deviceProfilePartial.c8y_DeviceProfile, { software }); this.updateDeviceProfile(deviceProfilePartial); }); } get isDeviceProfileEmpty() { const isSoftware = this.deviceProfile.c8y_DeviceProfile.software && this.deviceProfile.c8y_DeviceProfile.software.length > 0; const isFirmware = Boolean(this.deviceProfile.c8y_DeviceProfile.firmware); const isConfiguration = this.deviceProfile.c8y_DeviceProfile.configuration && this.deviceProfile.c8y_DeviceProfile.configuration.length > 0; return isSoftware || isFirmware || isConfiguration; } removeItem(removedItem, category) { const deviceProfilePartial = { c8y_DeviceProfile: this.deviceProfile.c8y_DeviceProfile }; const filtered = deviceProfilePartial.c8y_DeviceProfile[category].filter(item => !isEqual(removedItem, item)); deviceProfilePartial.c8y_DeviceProfile[category] = filtered; this.updateDeviceProfile(deviceProfilePartial); } removeFirmware() { delete this.deviceProfile.c8y_DeviceProfile.firmware; this.updateDeviceProfile({ c8y_DeviceProfile: this.deviceProfile.c8y_DeviceProfile }); } async updateDeviceProfile(partialDeviceProfile) { if (partialDeviceProfile.c8y_Filter && partialDeviceProfile.c8y_Filter.type === '') { delete partialDeviceProfile.c8y_Filter.type; } Object.assign(partialDeviceProfile, { id: this.deviceProfile.id }); try { const profileWithSoftwareTypeFragments = this.deviceProfileService.alignSoftwareTypeFragments(partialDeviceProfile, this.deviceProfile); const { data } = await this.inventoryService.update(profileWithSoftwareTypeFragments); this.deviceProfile = data; this.profileName = this.deviceProfile.name; this.alertService.success(gettext('Device profile changed.')); } catch (ex) { this.alertService.addServerFailure(ex); } } async getDeviceProfile(profileId) { try { const { data } = await this.inventoryService.detail(profileId); return data; } catch (ex) { this.alertService.addServerFailure(ex); } } getDeviceTypeQuery(repositoryType) { if (has(this.deviceProfile, 'c8y_Filter.type') && !isEmpty(this.deviceProfile.c8y_Filter.type)) { if (repositoryType === RepositoryType.CONFIGURATION) { return this.queriesUtil.addOrFilter({ deviceType: this.deviceProfile.c8y_Filter.type }, { __not: { __has: `deviceType` } }); } else { return this.queriesUtil.addOrFilter({ 'c8y_Filter.type': this.deviceProfile.c8y_Filter.type }, { __or: [{ 'c8y_Filter.type': '' }, { __not: { __has: `c8y_Filter.type` } }] }); } } return {}; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DeviceProfileComponent, deps: [{ token: i5.ActivatedRoute }, { token: i2.AlertService }, { token: i1.InventoryService }, { token: i4.BsModalService }, { token: i1$1.RepositoryService }, { token: DeviceProfileService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: DeviceProfileComponent, isStandalone: true, selector: "c8y-device-profile", providers: [ { provide: PRODUCT_EXPERIENCE_EVENT_SOURCE, useExisting: forwardRef(() => DeviceProfileComponent) } ], ngImport: i0, template: "<c8y-title>{{ profileName }}</c8y-title>\n\n<c8y-breadcrumb>\n <c8y-breadcrumb-item\n icon=\"c8y-management\"\n label=\"{{ 'Management' | translate }}\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n [icon]=\"'c8y-device-profile'\"\n [label]=\"'Device profiles' | translate\"\n [path]=\"'device-profiles'\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n icon=\"c8y-device-profile\"\n label=\"{{ profileName }}\"\n ></c8y-breadcrumb-item>\n</c8y-breadcrumb>\n\n<div\n class=\"row\"\n *ngIf=\"deviceProfile\"\n>\n <div class=\"col-lg-12 col-lg-max\">\n <div\n class=\"card card--fullpage\"\n *ngIf=\"deviceProfile\"\n >\n <div class=\"card-block bg-level-1 flex-no-shrink p-t-24 p-b-24 overflow-visible\">\n <div class=\"content-flex-70\">\n <div class=\"text-center\">\n <i class=\"c8y-icon-duocolor icon-48 c8y-icon c8y-icon-device-profile\"></i>\n <p>\n <small class=\"label label-info\">{{ 'Device profile' | translate }}</small>\n </p>\n </div>\n <div class=\"flex-grow col-10\">\n <div class=\"row\">\n <div class=\"col-md-4\">\n <form #editNameForm=\"ngForm\">\n <c8y-form-group>\n <label\n class=\"control-label\"\n translate\n >\n Name\n </label>\n <div class=\"input-group input-group-editable\">\n <input\n class=\"form-control\"\n placeholder=\"{{ 'e.g. My device profile' | translate }}\"\n name=\"name\"\n type=\"text\"\n required\n [(ngModel)]=\"deviceProfile.name\"\n data-cy=\"device-profile--add-device-profile-name\"\n size=\"{{ deviceProfile.name?.length || 1 }}\"\n />\n <span></span>\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Save' | translate }}\"\n type=\"button\"\n data-cy=\"device-profile--save\"\n (click)=\"\n updateDeviceProfile({ name: deviceProfile.name });\n editNameForm.form.markAsPristine()\n \"\n [disabled]=\"editNameForm.form.invalid\"\n c8yProductExperience\n inherit\n [actionData]=\"{ action: PRODUCT_EXPERIENCE.ACTIONS.SAVE }\"\n >\n {{ 'Save' | translate }}\n </button>\n </div>\n </div>\n </c8y-form-group>\n </form>\n </div>\n <div class=\"col-md-4\">\n <form #editTypeForm=\"ngForm\">\n <c8y-form-group>\n <label class=\"control-label\">\n {{ 'Device type' | translate }}\n <button\n class=\"btn-help\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"\n (DEVICE_TYPE_POPOVER | translate) +\n (isDeviceProfileEmpty\n ? ' ' + (DEVICE_TYPE_DISABLED_POPOVER | translate)\n : '')\n \"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n ></button>\n </label>\n <div class=\"input-group input-group-editable\">\n <input\n class=\"form-control\"\n placeholder=\"{{ 'e.g.' | translate }} c8y_Linux\"\n name=\"type\"\n type=\"text\"\n [(ngModel)]=\"deviceProfile.c8y_Filter.type\"\n data-cy=\"device-profile--device-type\"\n size=\"{{ deviceProfile.c8y_Filter.type?.length || 14 }}\"\n [disabled]=\"isDeviceProfileEmpty\"\n />\n <span></span>\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Save' | translate }}\"\n type=\"button\"\n (click)=\"\n updateDeviceProfile({\n c8y_Filter: { type: deviceProfile.c8y_Filter.type }\n });\n editTypeForm.form.markAsPristine()\n \"\n [disabled]=\"isDeviceProfileEmpty\"\n c8yProductExperience\n inherit\n [actionData]=\"{ action: PRODUCT_EXPERIENCE.ACTIONS.SAVE }\"\n >\n {{ 'Save' | translate }}\n </button>\n </div>\n </div>\n </c8y-form-group>\n </form>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"inner-scroll\">\n <div class=\"card-header separator-top-bottom bg-content sticky-top\">\n <div class=\"card-icon\">\n <i\n class=\"c8y-icon-duocolor\"\n [c8yIcon]=\"'c8y-firmware'\"\n ></i>\n </div>\n <div\n class=\"card-title\"\n translate\n >\n Firmware\n </div>\n </div>\n <div\n class=\"card-block p-t-0\"\n *ngIf=\"deviceProfile.c8y_DeviceProfile.firmware\"\n >\n <c8y-list-group>\n <c8y-li>\n <c8y-li-icon>\n <i [c8yIcon]=\"'c8y-firmware'\"></i>\n </c8y-li-icon>\n <c8y-li-body class=\"content-flex-50 m-l-4\">\n <div class=\"col-4\">\n <span\n class=\"text-truncate\"\n title=\"{{ deviceProfile.c8y_DeviceProfile.firmware.name }}\"\n >\n <span\n class=\"text-label-small m-r-8\"\n translate\n >\n Name\n </span>\n {{ deviceProfile.c8y_DeviceProfile.firmware.name }}\n </span>\n </div>\n <div class=\"col-4\"></div>\n <div class=\"col-3 d-flex a-i-center\">\n <span\n class=\"text-truncate\"\n title=\"{{ deviceProfile.c8y_DeviceProfile.firmware.version }}\"\n >\n <span\n class=\"text-label-small m-r-4\"\n translate\n >\n Version\n </span>\n {{ deviceProfile.c8y_DeviceProfile.firmware.version }}\n </span>\n <button\n class=\"btn btn-danger btn-xs visible-xs m-l-auto m-r-8 m-t-8\"\n title=\"{{ 'Remove`firmware`' | translate }}\"\n type=\"button\"\n (click)=\"removeFirmware()\"\n c8yProductExperience\n inherit\n [actionData]=\"{\n action: PRODUCT_EXPERIENCE.ACTIONS.REMOVE,\n fragment: PRODUCT_EXPERIENCE.FRAGMENTS.FIRMWARE\n }\"\n >\n <i c8yIcon=\"minus-circle\"></i>\n {{ 'Remove`firmware`' | translate }}\n </button>\n </div>\n <div class=\"m-l-auto p-r-8 hidden-xs\">\n <button\n class=\"btn btn-dot showOnHover text-danger\"\n [attr.aria-label]=\"'Remove`firmware`' | translate\"\n tooltip=\"{{ 'Remove`firmware`' | translate }}\"\n placement=\"right\"\n type=\"button\"\n [delay]=\"500\"\n (click)=\"removeFirmware()\"\n c8yProductExperience\n inherit\n [actionData]=\"{\n action: PRODUCT_EXPERIENCE.ACTIONS.REMOVE,\n fragment: PRODUCT_EXPERIENCE.FRAGMENTS.FIRMWARE\n }\"\n >\n <i c8yIcon=\"minus-circle\"></i>\n </button>\n </div>\n </c8y-li-body>\n </c8y-li>\n </c8y-list-group>\n </div>\n <div\n class=\"card-block p-t-16\"\n *ngIf=\"!deviceProfile.c8y_DeviceProfile.firmware\"\n >\n <c8y-ui-empty-state\n class=\"p-t-16 d-block\"\n icon=\"c8y-firmware\"\n [title]=\"'No firmware defined.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n </div>\n <div\n class=\"card-footer p-t-0\"\n *ngIf=\"!deviceProfile.c8y_DeviceProfile.firmware\"\n >\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Add firmware' | translate }}\"\n type=\"button\"\n data-cy=\"device-profile--Add-firmware-button\"\n (click)=\"addFirmware()\"\n c8yProductExperience\n inherit\n [actionData]=\"{\n action: PRODUCT_EXPERIENCE.ACTIONS.ADD,\n fragment: PRODUCT_EXPERIENCE.FRAGMENTS.FIRMWARE\n }\"\n >\n <i c8yIcon=\"plus-circle\"></i>\n {{ 'Add firmware' | translate }}\n </button>\n </div>\n\n <div class=\"card-header separator-top-bottom sticky-top bg-component\">\n <div class=\"card-icon\">\n <i\n class=\"c8y-icon-duocolor\"\n [c8yIcon]=\"'c8y-tools'\"\n ></i>\n </div>\n <div\n class=\"card-title\"\n translate\n >\n Software\n </div>\n </div>\n <div\n class=\"card-block p-t-0\"\n *ngIf=\"deviceProfile.c8y_DeviceProfile.software?.length > 0\"\n >\n <c8y-list-group>\n <c8y-li *ngFor=\"let software of deviceProfile.c8y_DeviceProfile.software\">\n <c8y-li-icon>\n <i [c8yIcon]=\"'c8y-tools'\"></i>\n </c8y-li-icon>\n <c8y-li-body class=\"content-flex-50 m-l-4\">\n <div class=\"col-4\">\n <span\n class=\"text-truncate-wrap\"\n title=\"{{ software.name }}\"\n >\n <span\n class=\"text-label-small m-r-8\"\n translate\n >\n Name\n </span>\n {{ software.name }}\n </span>\n </div>\n <div class=\"col-4\">\n <span\n class=\"text-truncate-wrap\"\n title=\"{{ software.name }}\"\n >\n <span\n class=\"text-label-small m-r-8\"\n translate\n >\n Type\n </span>\n <span\n class=\"label label-info m-l-4\"\n *ngIf=\"!!software.softwareType\"\n >\n {{ software.softwareType }}\n </span>\n </span>\n </div>\n <div class=\"col-3 d-flex a-i-center\">\n <span\n class=\"text-truncate-wrap\"\n title=\"{{ software.version }}\"\n >\n <span\n class=\"text-label-small m-r-8\"\n translate\n >\n Version\n </span>\n {{ software.version }}\n </span>\n <button\n class=\"btn btn-danger btn-xs visible-xs m-l-auto m-r-8 m-t-8\"\n title=\"{{ 'Remove`software`' | translate }}\"\n type=\"button\"\n ((click)=\"removeItem(software, 'software')\"\n c8yProductExperience\n inherit\n [actionData]=\"{\n action: PRODUCT_EXPERIENCE.ACTIONS.REMOVE,\n fragment: PRODUCT_EXPERIENCE.FRAGMENTS.SOFTWARE\n }\"\n >\n <i c8yIcon=\"minus-circle\"></i>\n {{ 'Remove`software`' | translate }}\n </button>\n </div>\n <div class=\"m-l-auto p-r-8 hidden-xs\">\n <button\n class=\"btn btn-dot showOnHover text-danger\"\n [attr.aria-label]=\"'Remove`software`' | translate\"\n tooltip=\"{{ 'Remove`software`' | translate }}\"\n placement=\"right\"\n type=\"button\"\n [delay]=\"500\"\n (click)=\"removeItem(software, 'software')\"\n c8yProductExperience\n inherit\n [actionData]=\"{\n action: PRODUCT_EXPERIENCE.ACTIONS.REMOVE,\n fragment: PRODUCT_EXPERIENCE.FRAGMENTS.SOFTWARE\n }\"\n >\n <i\n c8yIcon=\"minus-circle\"\n data-cy=\"device-profile--Remove-icon\"\n ></i>\n </button>\n </div>\n </c8y-li-body>\n </c8y-li>\n </c8y-list-group>\n </div>\n <div\n class=\"card-block p-t-16\"\n *ngIf=\"deviceProfile.c8y_DeviceProfile.software?.length === 0\"\n >\n <c8y-ui-empty-state\n icon=\"c8y-tools\"\n [title]=\"'No software defined.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n </div>\n <div class=\"card-footer p-t-0\">\n <button\n class=\"btn btn-default m-b-0\"\n title=\"{{ 'Add software' | translate }}\"\n type=\"button\"\n data-cy=\"device-profile--Add-software-button\"\n (click)=\"addSoftware()\"\n c8yProductExperience\n inherit\n [actionData]=\"{\n action: PRODUCT_EXPERIENCE.ACTIONS.ADD,\n fragment: PRODUCT_EXPERIENCE.FRAGMENTS.SOFTWARE\n }\"\n >\n <i c8yIcon=\"plus-circle\"></i>\n {{ 'Add software' | translate }}\n </button>\n </div>\n\n <div class=\"card-header separator-top-bottom bg-component sticky-top\">\n <div class=\"card-icon\">\n <i\n class=\"c8y-icon-duocolor\"\n [c8yIcon]=\"'gears'\"\n ></i>\n </div>\n <div\n class=\"card-title\"\n translate\n >\n Configuration\n </div>\n </div>\n <div\n class=\"card-block p-t-0\"\n *ngIf=\"deviceProfile.c8y_DeviceProfile.configuration?.length > 0\"\n >\n <c8y-list-group>\n <c8y-li *ngFor=\"let configuration of deviceProfile.c8y_DeviceProfile.configuration\">\n <c8y-li-icon>\n <i [c8yIcon]=\"'gears'\"></i>\n </c8y-li-icon>\n <c8y-li-body class=\"content-flex-50\">\n <div class=\"col-4\">\n <span\n class=\"text-truncate\"\n title=\"{{ configuration.name }}\"\n >\n <span\n class=\"text-label-small m-r-8\"\n translate\n >\n Name\n </span>\n {{ configuration.name }}\n </span>\n </div>\n <div class=\"col-4\">\n <span\n class=\"text-label-small m-r-8\"\n translate\n >\n Type\n </span>\n <span class=\"label label-info\">{{ configuration.type }}</span>\n <button\n class=\"btn btn-danger btn-xs visible-xs m-l-auto m-r-8 m-t-8\"\n title=\"{{ 'Remove`configuration`' | translate }}\"\n type=\"button\"\n (click)=\"removeItem(configuration, 'configuration')\"\n c8yProductExperience\n inherit\n [actionData]=\"{\n action: PRODUCT_EXPERIENCE.ACTIONS.REMOVE,\n fragment: PRODUCT_EXPERIENCE.FRAGMENTS.CONFGIURATION\n }\"\n >\n <i c8yIcon=\"minus-circle\"></i>\n {{ 'Remove`configuration`' | translate }}\n </b