Merge pull request #4695 from vvlladd28/improvement/ota-updates/select
[WIP] UI: Refactoring OTA update table and add supported OTA package external URL
This commit is contained in:
commit
f96ea353f6
@ -434,7 +434,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
|
|||||||
if (!firmware.getType().equals(OtaPackageType.FIRMWARE)) {
|
if (!firmware.getType().equals(OtaPackageType.FIRMWARE)) {
|
||||||
throw new DataValidationException("Can't assign firmware with type: " + firmware.getType());
|
throw new DataValidationException("Can't assign firmware with type: " + firmware.getType());
|
||||||
}
|
}
|
||||||
if (firmware.getData() == null) {
|
if (firmware.getData() == null && !firmware.hasUrl()) {
|
||||||
throw new DataValidationException("Can't assign firmware with empty data!");
|
throw new DataValidationException("Can't assign firmware with empty data!");
|
||||||
}
|
}
|
||||||
if (!firmware.getDeviceProfileId().equals(deviceProfile.getId())) {
|
if (!firmware.getDeviceProfileId().equals(deviceProfile.getId())) {
|
||||||
|
|||||||
@ -716,7 +716,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
|
|||||||
if (!firmware.getType().equals(OtaPackageType.FIRMWARE)) {
|
if (!firmware.getType().equals(OtaPackageType.FIRMWARE)) {
|
||||||
throw new DataValidationException("Can't assign firmware with type: " + firmware.getType());
|
throw new DataValidationException("Can't assign firmware with type: " + firmware.getType());
|
||||||
}
|
}
|
||||||
if (firmware.getData() == null) {
|
if (firmware.getData() == null && !firmware.hasUrl()) {
|
||||||
throw new DataValidationException("Can't assign firmware with empty data!");
|
throw new DataValidationException("Can't assign firmware with empty data!");
|
||||||
}
|
}
|
||||||
if (!firmware.getDeviceProfileId().equals(device.getDeviceProfileId())) {
|
if (!firmware.getDeviceProfileId().equals(device.getDeviceProfileId())) {
|
||||||
|
|||||||
@ -14,16 +14,21 @@
|
|||||||
/// limitations under the License.
|
/// limitations under the License.
|
||||||
///
|
///
|
||||||
|
|
||||||
import {Injectable} from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import {PageLink} from '@shared/models/page/page-link';
|
import { PageLink } from '@shared/models/page/page-link';
|
||||||
import {defaultHttpOptionsFromConfig, RequestConfig} from './http-utils';
|
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
|
||||||
import {Observable} from 'rxjs';
|
import { forkJoin, Observable, of, throwError } from 'rxjs';
|
||||||
import {PageData} from '@shared/models/page/page-data';
|
import { PageData } from '@shared/models/page/page-data';
|
||||||
import {DeviceProfile, DeviceProfileInfo, DeviceTransportType} from '@shared/models/device.models';
|
import { DeviceProfile, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models';
|
||||||
import {isDefinedAndNotNull, isEmptyStr} from '@core/utils';
|
import { isDefinedAndNotNull, isEmptyStr } from '@core/utils';
|
||||||
import {ObjectLwM2M, ServerSecurityConfig} from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
|
import { ObjectLwM2M, ServerSecurityConfig } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
|
||||||
import {SortOrder} from '@shared/models/page/sort-order';
|
import { SortOrder } from '@shared/models/page/sort-order';
|
||||||
|
import { OtaPackageService } from '@core/http/ota-package.service';
|
||||||
|
import { OtaUpdateType } from '@shared/models/ota-package.models';
|
||||||
|
import { mergeMap } from 'rxjs/operators';
|
||||||
|
import { DialogService } from '@core/services/dialog.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -31,7 +36,10 @@ import {SortOrder} from '@shared/models/page/sort-order';
|
|||||||
export class DeviceProfileService {
|
export class DeviceProfileService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient
|
private http: HttpClient,
|
||||||
|
private otaPackageService: OtaPackageService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private translate: TranslateService
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +78,34 @@ export class DeviceProfileService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public saveDeviceProfileAndConfirmOtaChange(originDeviceProfile: DeviceProfile, deviceProfile: DeviceProfile,
|
||||||
|
config?: RequestConfig): Observable<DeviceProfile> {
|
||||||
|
const tasks: Observable<number>[] = [];
|
||||||
|
if (originDeviceProfile?.id?.id && originDeviceProfile.firmwareId?.id !== deviceProfile.firmwareId?.id) {
|
||||||
|
tasks.push(this.otaPackageService.countUpdateDeviceAfterChangePackage(OtaUpdateType.FIRMWARE, deviceProfile.id.id));
|
||||||
|
} else {
|
||||||
|
tasks.push(of(0));
|
||||||
|
}
|
||||||
|
if (originDeviceProfile?.id?.id && originDeviceProfile.softwareId?.id !== deviceProfile.softwareId?.id) {
|
||||||
|
tasks.push(this.otaPackageService.countUpdateDeviceAfterChangePackage(OtaUpdateType.SOFTWARE, deviceProfile.id.id));
|
||||||
|
} else {
|
||||||
|
tasks.push(of(0));
|
||||||
|
}
|
||||||
|
return forkJoin(tasks).pipe(
|
||||||
|
mergeMap(([deviceFirmwareUpdate, deviceSoftwareUpdate]) => {
|
||||||
|
let text = '';
|
||||||
|
if (deviceFirmwareUpdate > 0) {
|
||||||
|
text += this.translate.instant('ota-update.change-firmware', {count: deviceFirmwareUpdate});
|
||||||
|
}
|
||||||
|
if (deviceSoftwareUpdate > 0) {
|
||||||
|
text += text.length ? ' ' : '';
|
||||||
|
text += this.translate.instant('ota-update.change-software', {count: deviceSoftwareUpdate});
|
||||||
|
}
|
||||||
|
return text !== '' ? this.dialogService.confirm('', text, null, this.translate.instant('common.proceed')) : of(true);
|
||||||
|
}),
|
||||||
|
mergeMap((update) => update ? this.saveDeviceProfile(deviceProfile, config) : throwError('Canceled saving device profiles')));
|
||||||
|
}
|
||||||
|
|
||||||
public saveDeviceProfile(deviceProfile: DeviceProfile, config?: RequestConfig): Observable<DeviceProfile> {
|
public saveDeviceProfile(deviceProfile: DeviceProfile, config?: RequestConfig): Observable<DeviceProfile> {
|
||||||
return this.http.post<DeviceProfile>('/api/deviceProfile', deviceProfile, defaultHttpOptionsFromConfig(config));
|
return this.http.post<DeviceProfile>('/api/deviceProfile', deviceProfile, defaultHttpOptionsFromConfig(config));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export class OtaPackageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getOtaPackagesInfoByDeviceProfileId(pageLink: PageLink, deviceProfileId: string, type: OtaUpdateType,
|
public getOtaPackagesInfoByDeviceProfileId(pageLink: PageLink, deviceProfileId: string, type: OtaUpdateType,
|
||||||
hasData = true, config?: RequestConfig): Observable<PageData<OtaPackageInfo>> {
|
config?: RequestConfig): Observable<PageData<OtaPackageInfo>> {
|
||||||
const url = `/api/otaPackages/${deviceProfileId}/${type}${pageLink.toQuery()}`;
|
const url = `/api/otaPackages/${deviceProfileId}/${type}${pageLink.toQuery()}`;
|
||||||
return this.http.get<PageData<OtaPackageInfo>>(url, defaultHttpOptionsFromConfig(config));
|
return this.http.get<PageData<OtaPackageInfo>>(url, defaultHttpOptionsFromConfig(config));
|
||||||
}
|
}
|
||||||
@ -120,4 +120,8 @@ export class OtaPackageService {
|
|||||||
return this.http.delete(`/api/otaPackage/${otaPackageId}`, defaultHttpOptionsFromConfig(config));
|
return this.http.delete(`/api/otaPackage/${otaPackageId}`, defaultHttpOptionsFromConfig(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public countUpdateDeviceAfterChangePackage(type: OtaUpdateType, deviceProfileId: string, config?: RequestConfig): Observable<number> {
|
||||||
|
return this.http.get<number>(`/api/devices/count/${type}?deviceProfileId=${deviceProfileId}`, defaultHttpOptionsFromConfig(config));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -163,8 +163,34 @@
|
|||||||
*matCellDef="let entity; let row = index"
|
*matCellDef="let entity; let row = index"
|
||||||
[matTooltip]="cellTooltip(entity, column, row)"
|
[matTooltip]="cellTooltip(entity, column, row)"
|
||||||
matTooltipPosition="above"
|
matTooltipPosition="above"
|
||||||
[innerHTML]="cellContent(entity, column, row)"
|
|
||||||
[ngStyle]="cellStyle(entity, column, row)">
|
[ngStyle]="cellStyle(entity, column, row)">
|
||||||
|
<span [innerHTML]="cellContent(entity, column, row)"></span>
|
||||||
|
<ng-template [ngIf]="column.actionCell">
|
||||||
|
<ng-container [ngSwitch]="column.actionCell.type">
|
||||||
|
<ng-template [ngSwitchCase]="cellActionType.COPY_BUTTON">
|
||||||
|
<tb-copy-button
|
||||||
|
[disabled]="isLoading$ | async"
|
||||||
|
[fxShow]="column.actionCell.isEnabled(entity)"
|
||||||
|
[copyText]="column.actionCell.onAction(null, entity)"
|
||||||
|
tooltipText="{{ column.actionCell.nameFunction ? column.actionCell.nameFunction(entity) : column.actionCell.name }}"
|
||||||
|
tooltipPosition="above"
|
||||||
|
[icon]="column.actionCell.icon"
|
||||||
|
[mdiIcon]="column.actionCell.mdiIcon" [style]="column.actionCell.style">
|
||||||
|
</tb-copy-button>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template ngSwitchDefault>
|
||||||
|
<button mat-icon-button [disabled]="isLoading$ | async"
|
||||||
|
[fxShow]="column.actionCell.isEnabled(entity)"
|
||||||
|
matTooltip="{{ column.actionCell.nameFunction ? column.actionCell.nameFunction(entity) : column.actionCell.name }}"
|
||||||
|
matTooltipPosition="above"
|
||||||
|
(click)="column.actionCell.onAction($event, entity)">
|
||||||
|
<mat-icon [svgIcon]="column.actionCell.mdiIcon" [ngStyle]="column.actionCell.style">
|
||||||
|
{{column.actionCell.icon}}
|
||||||
|
</mat-icon>
|
||||||
|
</button>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container [matColumnDef]="column.key" *ngFor="let column of actionColumns; trackBy: trackByColumnKey;">
|
<ng-container [matColumnDef]="column.key" *ngFor="let column of actionColumns; trackBy: trackByColumnKey;">
|
||||||
|
|||||||
@ -43,7 +43,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import { BaseData, HasId } from '@shared/models/base-data';
|
import { BaseData, HasId } from '@shared/models/base-data';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
CellActionDescriptor,
|
CellActionDescriptor, CellActionDescriptorType,
|
||||||
EntityActionTableColumn,
|
EntityActionTableColumn,
|
||||||
EntityColumn,
|
EntityColumn,
|
||||||
EntityTableColumn,
|
EntityTableColumn,
|
||||||
@ -104,6 +104,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
|
|||||||
timewindow: Timewindow;
|
timewindow: Timewindow;
|
||||||
dataSource: EntitiesDataSource<BaseData<HasId>>;
|
dataSource: EntitiesDataSource<BaseData<HasId>>;
|
||||||
|
|
||||||
|
cellActionType = CellActionDescriptorType;
|
||||||
|
|
||||||
isDetailsOpen = false;
|
isDetailsOpen = false;
|
||||||
detailsPanelOpened = new EventEmitter<boolean>();
|
detailsPanelOpened = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
|||||||
@ -280,7 +280,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV
|
|||||||
editingEntity.additionalInfo =
|
editingEntity.additionalInfo =
|
||||||
mergeDeep((this.editingEntity as any).additionalInfo, this.entityComponent.entityFormValue()?.additionalInfo);
|
mergeDeep((this.editingEntity as any).additionalInfo, this.entityComponent.entityFormValue()?.additionalInfo);
|
||||||
}
|
}
|
||||||
this.entitiesTableConfig.saveEntity(editingEntity).subscribe(
|
this.entitiesTableConfig.saveEntity(editingEntity, this.editingEntity).subscribe(
|
||||||
(entity) => {
|
(entity) => {
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
this.entityComponent.entity = entity;
|
this.entityComponent.entity = entity;
|
||||||
|
|||||||
@ -66,5 +66,5 @@
|
|||||||
<mat-error *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').hasError('required')">
|
<mat-error *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').hasError('required')">
|
||||||
{{ 'device-profile.device-profile-required' | translate }}
|
{{ 'device-profile.device-profile-required' | translate }}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
<mat-hint *ngIf="!!hint">{{ hint | translate }}</mat-hint>
|
<mat-hint *ngIf="hint && !disabled">{{ hint | translate }}</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|||||||
@ -90,7 +90,7 @@ export class DeviceProfileDialogComponent extends
|
|||||||
this.submitted = true;
|
this.submitted = true;
|
||||||
if (this.deviceProfileComponent.entityForm.valid) {
|
if (this.deviceProfileComponent.entityForm.valid) {
|
||||||
this.deviceProfile = {...this.deviceProfile, ...this.deviceProfileComponent.entityFormValue()};
|
this.deviceProfile = {...this.deviceProfile, ...this.deviceProfileComponent.entityFormValue()};
|
||||||
this.deviceProfileService.saveDeviceProfile(this.deviceProfile).subscribe(
|
this.deviceProfileService.saveDeviceProfileAndConfirmOtaChange(this.deviceProfile, this.deviceProfile).subscribe(
|
||||||
(deviceProfile) => {
|
(deviceProfile) => {
|
||||||
this.dialogRef.close(deviceProfile);
|
this.dialogRef.close(deviceProfile);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,7 +37,7 @@ export type EntityStringFunction<T extends BaseData<HasId>> = (entity: T) => str
|
|||||||
export type EntityVoidFunction<T extends BaseData<HasId>> = (entity: T) => void;
|
export type EntityVoidFunction<T extends BaseData<HasId>> = (entity: T) => void;
|
||||||
export type EntityIdsVoidFunction<T extends BaseData<HasId>> = (ids: HasUUID[]) => void;
|
export type EntityIdsVoidFunction<T extends BaseData<HasId>> = (ids: HasUUID[]) => void;
|
||||||
export type EntityCountStringFunction = (count: number) => string;
|
export type EntityCountStringFunction = (count: number) => string;
|
||||||
export type EntityTwoWayOperation<T extends BaseData<HasId>> = (entity: T) => Observable<T>;
|
export type EntityTwoWayOperation<T extends BaseData<HasId>> = (entity: T, originalEntity?: T) => Observable<T>;
|
||||||
export type EntityByIdOperation<T extends BaseData<HasId>> = (id: HasUUID) => Observable<T>;
|
export type EntityByIdOperation<T extends BaseData<HasId>> = (id: HasUUID) => Observable<T>;
|
||||||
export type EntityIdOneWayOperation = (id: HasUUID) => Observable<any>;
|
export type EntityIdOneWayOperation = (id: HasUUID) => Observable<any>;
|
||||||
export type EntityActionFunction<T extends BaseData<HasId>> = (action: EntityAction<T>) => boolean;
|
export type EntityActionFunction<T extends BaseData<HasId>> = (action: EntityAction<T>) => boolean;
|
||||||
@ -48,6 +48,9 @@ export type CellContentFunction<T extends BaseData<HasId>> = (entity: T, key: st
|
|||||||
export type CellTooltipFunction<T extends BaseData<HasId>> = (entity: T, key: string) => string | undefined;
|
export type CellTooltipFunction<T extends BaseData<HasId>> = (entity: T, key: string) => string | undefined;
|
||||||
export type HeaderCellStyleFunction<T extends BaseData<HasId>> = (key: string) => object;
|
export type HeaderCellStyleFunction<T extends BaseData<HasId>> = (key: string) => object;
|
||||||
export type CellStyleFunction<T extends BaseData<HasId>> = (entity: T, key: string) => object;
|
export type CellStyleFunction<T extends BaseData<HasId>> = (entity: T, key: string) => object;
|
||||||
|
export type CopyCellContent<T extends BaseData<HasId>> = (entity: T, key: string, length: number) => object;
|
||||||
|
|
||||||
|
export enum CellActionDescriptorType { 'DEFAULT', 'COPY_BUTTON'}
|
||||||
|
|
||||||
export interface CellActionDescriptor<T extends BaseData<HasId>> {
|
export interface CellActionDescriptor<T extends BaseData<HasId>> {
|
||||||
name: string;
|
name: string;
|
||||||
@ -56,7 +59,8 @@ export interface CellActionDescriptor<T extends BaseData<HasId>> {
|
|||||||
mdiIcon?: string;
|
mdiIcon?: string;
|
||||||
style?: any;
|
style?: any;
|
||||||
isEnabled: (entity: T) => boolean;
|
isEnabled: (entity: T) => boolean;
|
||||||
onAction: ($event: MouseEvent, entity: T) => void;
|
onAction: ($event: MouseEvent, entity: T) => any;
|
||||||
|
type?: CellActionDescriptorType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupActionDescriptor<T extends BaseData<HasId>> {
|
export interface GroupActionDescriptor<T extends BaseData<HasId>> {
|
||||||
@ -95,7 +99,8 @@ export class EntityTableColumn<T extends BaseData<HasId>> extends BaseEntityTabl
|
|||||||
public sortable: boolean = true,
|
public sortable: boolean = true,
|
||||||
public headerCellStyleFunction: HeaderCellStyleFunction<T> = () => ({}),
|
public headerCellStyleFunction: HeaderCellStyleFunction<T> = () => ({}),
|
||||||
public cellTooltipFunction: CellTooltipFunction<T> = () => undefined,
|
public cellTooltipFunction: CellTooltipFunction<T> = () => undefined,
|
||||||
public isNumberColumn: boolean = false) {
|
public isNumberColumn: boolean = false,
|
||||||
|
public actionCell: CellActionDescriptor<T> = null) {
|
||||||
super('content', key, title, width, sortable);
|
super('content', key, title, width, sortable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -173,7 +178,7 @@ export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = P
|
|||||||
deleteEntitiesTitle: EntityCountStringFunction = () => '';
|
deleteEntitiesTitle: EntityCountStringFunction = () => '';
|
||||||
deleteEntitiesContent: EntityCountStringFunction = () => '';
|
deleteEntitiesContent: EntityCountStringFunction = () => '';
|
||||||
loadEntity: EntityByIdOperation<T | L> = () => of();
|
loadEntity: EntityByIdOperation<T | L> = () => of();
|
||||||
saveEntity: EntityTwoWayOperation<T> = (entity) => of(entity);
|
saveEntity: EntityTwoWayOperation<T> = (entity, originalEntity) => of(entity);
|
||||||
deleteEntity: EntityIdOneWayOperation = () => of();
|
deleteEntity: EntityIdOneWayOperation = () => of();
|
||||||
entitiesFetchFunction: EntitiesFetchFunction<L, P> = () => of(emptyPageData<L>());
|
entitiesFetchFunction: EntitiesFetchFunction<L, P> = () => of(emptyPageData<L>());
|
||||||
onEntityAction: EntityActionFunction<T> = () => false;
|
onEntityAction: EntityActionFunction<T> = () => false;
|
||||||
|
|||||||
@ -104,7 +104,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
|
|||||||
|
|
||||||
this.config.entitiesFetchFunction = pageLink => this.deviceProfileService.getDeviceProfiles(pageLink);
|
this.config.entitiesFetchFunction = pageLink => this.deviceProfileService.getDeviceProfiles(pageLink);
|
||||||
this.config.loadEntity = id => this.deviceProfileService.getDeviceProfile(id.id);
|
this.config.loadEntity = id => this.deviceProfileService.getDeviceProfile(id.id);
|
||||||
this.config.saveEntity = deviceProfile => this.deviceProfileService.saveDeviceProfile(deviceProfile);
|
this.config.saveEntity = (deviceProfile, originDeviceProfile) =>
|
||||||
|
this.deviceProfileService.saveDeviceProfileAndConfirmOtaChange(originDeviceProfile, deviceProfile);
|
||||||
this.config.deleteEntity = id => this.deviceProfileService.deleteDeviceProfile(id.id);
|
this.config.deleteEntity = id => this.deviceProfileService.deleteDeviceProfile(id.id);
|
||||||
this.config.onEntityAction = action => this.onDeviceProfileAction(action);
|
this.config.onEntityAction = action => this.onDeviceProfileAction(action);
|
||||||
this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default;
|
this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default;
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Resolve } from '@angular/router';
|
import { Resolve } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
|
CellActionDescriptorType,
|
||||||
DateEntityTableColumn,
|
DateEntityTableColumn,
|
||||||
EntityTableColumn,
|
EntityTableColumn,
|
||||||
EntityTableConfig
|
EntityTableConfig
|
||||||
@ -35,6 +36,8 @@ import { PageLink } from '@shared/models/page/page-link';
|
|||||||
import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component';
|
import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component';
|
||||||
import { EntityAction } from '@home/models/entity/entity-component.models';
|
import { EntityAction } from '@home/models/entity/entity-component.models';
|
||||||
import { FileSizePipe } from '@shared/pipe/file-size.pipe';
|
import { FileSizePipe } from '@shared/pipe/file-size.pipe';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { AppState } from '@core/core.state';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<OtaPackage, PageLink, OtaPackageInfo>> {
|
export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<OtaPackage, PageLink, OtaPackageInfo>> {
|
||||||
@ -44,6 +47,7 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot
|
|||||||
|
|
||||||
constructor(private translate: TranslateService,
|
constructor(private translate: TranslateService,
|
||||||
private datePipe: DatePipe,
|
private datePipe: DatePipe,
|
||||||
|
private store: Store<AppState>,
|
||||||
private otaPackageService: OtaPackageService,
|
private otaPackageService: OtaPackageService,
|
||||||
private fileSize: FileSizePipe) {
|
private fileSize: FileSizePipe) {
|
||||||
this.config.entityType = EntityType.OTA_PACKAGE;
|
this.config.entityType = EntityType.OTA_PACKAGE;
|
||||||
@ -55,25 +59,50 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot
|
|||||||
|
|
||||||
this.config.columns.push(
|
this.config.columns.push(
|
||||||
new DateEntityTableColumn<OtaPackageInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
|
new DateEntityTableColumn<OtaPackageInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
|
||||||
new EntityTableColumn<OtaPackageInfo>('title', 'ota-update.title', '25%'),
|
new EntityTableColumn<OtaPackageInfo>('title', 'ota-update.title', '20%'),
|
||||||
new EntityTableColumn<OtaPackageInfo>('version', 'ota-update.version', '25%'),
|
new EntityTableColumn<OtaPackageInfo>('version', 'ota-update.version', '20%'),
|
||||||
new EntityTableColumn<OtaPackageInfo>('type', 'ota-update.package-type', '25%', entity => {
|
new EntityTableColumn<OtaPackageInfo>('type', 'ota-update.package-type', '20%', entity => {
|
||||||
return this.translate.instant(OtaUpdateTypeTranslationMap.get(entity.type));
|
return this.translate.instant(OtaUpdateTypeTranslationMap.get(entity.type));
|
||||||
}),
|
}),
|
||||||
new EntityTableColumn<OtaPackageInfo>('fileName', 'ota-update.file-name', '25%'),
|
new EntityTableColumn<OtaPackageInfo>('url', 'ota-update.direct-url', '20%', entity => {
|
||||||
new EntityTableColumn<OtaPackageInfo>('dataSize', 'ota-update.file-size', '70px', entity => {
|
return entity.url && entity.url.length > 20 ? `${entity.url.slice(0, 20)}…` : '';
|
||||||
return this.fileSize.transform(entity.dataSize || 0);
|
}, () => ({}), true, () => ({}), () => undefined, false,
|
||||||
|
{
|
||||||
|
name: this.translate.instant('ota-update.copy-direct-url'),
|
||||||
|
icon: 'content_paste',
|
||||||
|
style: {
|
||||||
|
'font-size': '16px',
|
||||||
|
color: 'rgba(0,0,0,.87)'
|
||||||
|
},
|
||||||
|
isEnabled: (otaPackage) => !!otaPackage.url,
|
||||||
|
onAction: ($event, entity) => entity.url,
|
||||||
|
type: CellActionDescriptorType.COPY_BUTTON
|
||||||
}),
|
}),
|
||||||
new EntityTableColumn<OtaPackageInfo>('checksum', 'ota-update.checksum', '540px', entity => {
|
new EntityTableColumn<OtaPackageInfo>('fileName', 'ota-update.file-name', '20%'),
|
||||||
return `${ChecksumAlgorithmTranslationMap.get(entity.checksumAlgorithm)}: ${entity.checksum}`;
|
new EntityTableColumn<OtaPackageInfo>('dataSize', 'ota-update.file-size', '70px', entity => {
|
||||||
}, () => ({}), false)
|
return entity.dataSize ? this.fileSize.transform(entity.dataSize) : '';
|
||||||
|
}),
|
||||||
|
new EntityTableColumn<OtaPackageInfo>('checksum', 'ota-update.checksum', '220px', entity => {
|
||||||
|
return entity.checksum ? this.checksumText(entity) : '';
|
||||||
|
}, () => ({}), true, () => ({}), () => undefined, false,
|
||||||
|
{
|
||||||
|
name: this.translate.instant('ota-update.copy-checksum'),
|
||||||
|
icon: 'content_paste',
|
||||||
|
style: {
|
||||||
|
'font-size': '16px',
|
||||||
|
color: 'rgba(0,0,0,.87)'
|
||||||
|
},
|
||||||
|
isEnabled: (otaPackage) => !!otaPackage.checksum,
|
||||||
|
onAction: ($event, entity) => entity.checksum,
|
||||||
|
type: CellActionDescriptorType.COPY_BUTTON
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.config.cellActionDescriptors.push(
|
this.config.cellActionDescriptors.push(
|
||||||
{
|
{
|
||||||
name: this.translate.instant('ota-update.download'),
|
name: this.translate.instant('ota-update.download'),
|
||||||
icon: 'file_download',
|
icon: 'file_download',
|
||||||
isEnabled: (otaPackage) => otaPackage.hasData,
|
isEnabled: (otaPackage) => otaPackage.hasData && !otaPackage.url,
|
||||||
onAction: ($event, entity) => this.exportPackage($event, entity)
|
onAction: ($event, entity) => this.exportPackage($event, entity)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -101,8 +130,20 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot
|
|||||||
if ($event) {
|
if ($event) {
|
||||||
$event.stopPropagation();
|
$event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
if (otaPackageInfo.url) {
|
||||||
|
window.open(otaPackageInfo.url, '_blank');
|
||||||
|
} else {
|
||||||
this.otaPackageService.downloadOtaPackage(otaPackageInfo.id.id).subscribe();
|
this.otaPackageService.downloadOtaPackage(otaPackageInfo.id.id).subscribe();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checksumText(entity): string {
|
||||||
|
let text = `${ChecksumAlgorithmTranslationMap.get(entity.checksumAlgorithm)}: ${entity.checksum}`;
|
||||||
|
if (text.length > 20) {
|
||||||
|
text = `${text.slice(0, 20)}…`;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
onPackageAction(action: EntityAction<OtaPackageInfo>): boolean {
|
onPackageAction(action: EntityAction<OtaPackageInfo>): boolean {
|
||||||
switch (action.action) {
|
switch (action.action) {
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
-->
|
-->
|
||||||
<div class="tb-details-buttons" fxLayout.xs="column">
|
<div class="tb-details-buttons" fxLayout.xs="column">
|
||||||
<button mat-raised-button color="primary" fxFlex.xs
|
<button mat-raised-button color="primary" fxFlex.xs
|
||||||
[disabled]="(isLoading$ | async) || !entity?.hasData"
|
[disabled]="(isLoading$ | async) || !(entity?.hasData && !entity?.url)"
|
||||||
(click)="onEntityAction($event, 'uploadPackage')"
|
(click)="onEntityAction($event, 'uploadPackage')"
|
||||||
[fxShow]="!isEdit">
|
[fxShow]="!isEdit">
|
||||||
{{ 'ota-update.download' | translate }}
|
{{ 'ota-update.download' | translate }}
|
||||||
@ -41,15 +41,23 @@
|
|||||||
ngxClipboard
|
ngxClipboard
|
||||||
(cbOnSuccess)="onPackageChecksumCopied()"
|
(cbOnSuccess)="onPackageChecksumCopied()"
|
||||||
[cbContent]="entity?.checksum"
|
[cbContent]="entity?.checksum"
|
||||||
[fxShow]="!isEdit">
|
[fxShow]="!isEdit && entity?.checksum">
|
||||||
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
|
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
|
||||||
<span translate>ota-update.copy-checksum</span>
|
<span translate>ota-update.copy-checksum</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button mat-raised-button
|
||||||
|
ngxClipboard
|
||||||
|
(cbOnSuccess)="onPackageDirectUrlCopied()"
|
||||||
|
[cbContent]="entity?.url"
|
||||||
|
[fxShow]="!isEdit && entity?.url">
|
||||||
|
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
|
||||||
|
<span translate>ota-update.copy-direct-url</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mat-padding" fxLayout="column" fxLayoutGap="8px">
|
<div class="mat-padding" fxLayout="column" fxLayoutGap="8px">
|
||||||
<form [formGroup]="entityForm">
|
<form [formGroup]="entityForm">
|
||||||
<fieldset [disabled]="(isLoading$ | async) || !isEdit" fxLayout="column" fxLayoutGap="8px">
|
<fieldset [disabled]="(isLoading$ | async) || !isEdit" fxLayout="column">
|
||||||
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
|
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
|
||||||
<mat-form-field class="mat-block" fxFlex="45">
|
<mat-form-field class="mat-block" fxFlex="45">
|
||||||
<mat-label translate>ota-update.title</mat-label>
|
<mat-label translate>ota-update.title</mat-label>
|
||||||
@ -69,6 +77,7 @@
|
|||||||
<tb-device-profile-autocomplete
|
<tb-device-profile-autocomplete
|
||||||
formControlName="deviceProfileId"
|
formControlName="deviceProfileId"
|
||||||
required
|
required
|
||||||
|
[ngStyle]="{'padding-bottom': isAdd ? '16px': 0}"
|
||||||
[hint]="'ota-update.chose-compatible-device-profile'"
|
[hint]="'ota-update.chose-compatible-device-profile'"
|
||||||
[editProfileEnabled]="false"
|
[editProfileEnabled]="false"
|
||||||
[addNewProfile]="false"
|
[addNewProfile]="false"
|
||||||
@ -82,8 +91,27 @@
|
|||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div class="mat-caption" translate *ngIf="isAdd">ota-update.warning-after-save-no-edit</div>
|
<section *ngIf="isAdd">
|
||||||
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayoutGap.sm="8px" fxLayout.xs="column" fxLayout.md="column">
|
<div class="mat-caption" style="margin: -8px 0 8px;" translate>ota-update.warning-after-save-no-edit</div>
|
||||||
|
<mat-radio-group formControlName="resource" fxLayoutGap="16px">
|
||||||
|
<mat-radio-button value="file">Upload binary file</mat-radio-button>
|
||||||
|
<mat-radio-button value="url">Use external URL</mat-radio-button>
|
||||||
|
</mat-radio-group>
|
||||||
|
</section>
|
||||||
|
<section *ngIf="entityForm.get('resource').value === 'file'">
|
||||||
|
<section *ngIf="isAdd">
|
||||||
|
<tb-file-input
|
||||||
|
formControlName="file"
|
||||||
|
workFromFileObj="true"
|
||||||
|
[required]="entityForm.get('resource').value === 'file'"
|
||||||
|
dropLabel="{{'ota-update.drop-file' | translate}}">
|
||||||
|
</tb-file-input>
|
||||||
|
<mat-checkbox formControlName="generateChecksum" style="margin-top: 16px">
|
||||||
|
{{ 'ota-update.auto-generate-checksum' | translate }}
|
||||||
|
</mat-checkbox>
|
||||||
|
</section>
|
||||||
|
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayoutGap.sm="8px"
|
||||||
|
fxLayout.xs="column" fxLayout.md="column" *ngIf="!(isAdd && this.entityForm.get('generateChecksum').value)">
|
||||||
<mat-form-field class="mat-block" fxFlex="33">
|
<mat-form-field class="mat-block" fxFlex="33">
|
||||||
<mat-label translate>ota-update.checksum-algorithm</mat-label>
|
<mat-label translate>ota-update.checksum-algorithm</mat-label>
|
||||||
<mat-select formControlName="checksumAlgorithm">
|
<mat-select formControlName="checksumAlgorithm">
|
||||||
@ -94,33 +122,38 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="mat-block" fxFlex>
|
<mat-form-field class="mat-block" fxFlex>
|
||||||
<mat-label translate>ota-update.checksum</mat-label>
|
<mat-label translate>ota-update.checksum</mat-label>
|
||||||
<input matInput formControlName="checksum" type="text" [readonly]="!isAdd">
|
<input matInput formControlName="checksum" type="text">
|
||||||
|
<mat-hint *ngIf="isAdd" translate>ota-update.checksum-hint</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<section *ngIf="isAdd">
|
|
||||||
<tb-file-input
|
|
||||||
formControlName="file"
|
|
||||||
workFromFileObj="true"
|
|
||||||
required
|
|
||||||
dropLabel="{{'ota-update.drop-file' | translate}}">
|
|
||||||
</tb-file-input>
|
|
||||||
</section>
|
|
||||||
<section *ngIf="!isAdd">
|
<section *ngIf="!isAdd">
|
||||||
<div fxLayout="row" fxLayoutGap.gt-md="8px" fxLayoutGap.sm="8px" fxLayout.xs="column" fxLayout.md="column">
|
<div fxLayout="row" fxLayoutGap.gt-md="8px" fxLayoutGap.sm="8px" fxLayout.xs="column" fxLayout.md="column">
|
||||||
<mat-form-field class="mat-block" fxFlex="33">
|
<mat-form-field class="mat-block" fxFlex="33">
|
||||||
<mat-label translate>ota-update.file-name</mat-label>
|
<mat-label translate>ota-update.file-name</mat-label>
|
||||||
<input matInput formControlName="fileName" type="text" readonly>
|
<input matInput formControlName="fileName" type="text">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="mat-block" fxFlex>
|
<mat-form-field class="mat-block" fxFlex>
|
||||||
<mat-label translate>ota-update.file-size-bytes</mat-label>
|
<mat-label translate>ota-update.file-size-bytes</mat-label>
|
||||||
<input matInput formControlName="dataSize" type="text" readonly>
|
<input matInput formControlName="dataSize" type="text">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="mat-block" fxFlex>
|
<mat-form-field class="mat-block" fxFlex>
|
||||||
<mat-label translate>ota-update.content-type</mat-label>
|
<mat-label translate>ota-update.content-type</mat-label>
|
||||||
<input matInput formControlName="contentType" type="text" readonly>
|
<input matInput formControlName="contentType" type="text">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</section>
|
||||||
|
<section *ngIf="entityForm.get('resource').value === 'url'" style="margin-top: 8px">
|
||||||
|
<mat-form-field class="mat-block">
|
||||||
|
<mat-label translate>ota-update.direct-url</mat-label>
|
||||||
|
<input matInput formControlName="url"
|
||||||
|
type="text"
|
||||||
|
[required]="entityForm.get('resource').value === 'url'">
|
||||||
|
<mat-error *ngIf="entityForm.get('url').hasError('required')" translate>
|
||||||
|
ota-update.direct-url-required
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</section>
|
||||||
<div formGroupName="additionalInfo">
|
<div formGroupName="additionalInfo">
|
||||||
<mat-form-field class="mat-block">
|
<mat-form-field class="mat-block">
|
||||||
<mat-label translate>ota-update.description</mat-label>
|
<mat-label translate>ota-update.description</mat-label>
|
||||||
|
|||||||
@ -30,6 +30,8 @@ import {
|
|||||||
OtaUpdateTypeTranslationMap
|
OtaUpdateTypeTranslationMap
|
||||||
} from '@shared/models/ota-package.models';
|
} from '@shared/models/ota-package.models';
|
||||||
import { ActionNotificationShow } from '@core/notification/notification.actions';
|
import { ActionNotificationShow } from '@core/notification/notification.actions';
|
||||||
|
import { filter, takeUntil } from 'rxjs/operators';
|
||||||
|
import { isNotEmptyStr } from '@core/utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-ota-update',
|
selector: 'tb-ota-update',
|
||||||
@ -52,6 +54,26 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
|
|||||||
super(store, fb, entityValue, entitiesTableConfigValue);
|
super(store, fb, entityValue, entitiesTableConfigValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.entityForm.get('resource').valueChanges.pipe(
|
||||||
|
filter(() => this.isAdd),
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
).subscribe((resource) => {
|
||||||
|
if (resource === 'file') {
|
||||||
|
this.entityForm.get('url').clearValidators();
|
||||||
|
this.entityForm.get('file').setValidators(Validators.required);
|
||||||
|
this.entityForm.get('url').updateValueAndValidity({emitEvent: false});
|
||||||
|
this.entityForm.get('file').updateValueAndValidity({emitEvent: false});
|
||||||
|
} else {
|
||||||
|
this.entityForm.get('file').clearValidators();
|
||||||
|
this.entityForm.get('url').setValidators(Validators.required);
|
||||||
|
this.entityForm.get('file').updateValueAndValidity({emitEvent: false});
|
||||||
|
this.entityForm.get('url').updateValueAndValidity({emitEvent: false});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
super.ngOnDestroy();
|
super.ngOnDestroy();
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
@ -74,6 +96,8 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
|
|||||||
deviceProfileId: [entity ? entity.deviceProfileId : null, Validators.required],
|
deviceProfileId: [entity ? entity.deviceProfileId : null, Validators.required],
|
||||||
checksumAlgorithm: [entity && entity.checksumAlgorithm ? entity.checksumAlgorithm : ChecksumAlgorithm.SHA256],
|
checksumAlgorithm: [entity && entity.checksumAlgorithm ? entity.checksumAlgorithm : ChecksumAlgorithm.SHA256],
|
||||||
checksum: [entity ? entity.checksum : '', Validators.maxLength(1020)],
|
checksum: [entity ? entity.checksum : '', Validators.maxLength(1020)],
|
||||||
|
url: [entity ? entity.url : ''],
|
||||||
|
resource: ['file'],
|
||||||
additionalInfo: this.fb.group(
|
additionalInfo: this.fb.group(
|
||||||
{
|
{
|
||||||
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
|
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
|
||||||
@ -82,6 +106,7 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
|
|||||||
});
|
});
|
||||||
if (this.isAdd) {
|
if (this.isAdd) {
|
||||||
form.addControl('file', this.fb.control(null, Validators.required));
|
form.addControl('file', this.fb.control(null, Validators.required));
|
||||||
|
form.addControl('generateChecksum', this.fb.control(true));
|
||||||
} else {
|
} else {
|
||||||
form.addControl('fileName', this.fb.control(null));
|
form.addControl('fileName', this.fb.control(null));
|
||||||
form.addControl('dataSize', this.fb.control(null));
|
form.addControl('dataSize', this.fb.control(null));
|
||||||
@ -101,6 +126,8 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
|
|||||||
fileName: entity.fileName,
|
fileName: entity.fileName,
|
||||||
dataSize: entity.dataSize,
|
dataSize: entity.dataSize,
|
||||||
contentType: entity.contentType,
|
contentType: entity.contentType,
|
||||||
|
url: entity.url,
|
||||||
|
resource: isNotEmptyStr(entity.url) ? 'url' : 'file',
|
||||||
additionalInfo: {
|
additionalInfo: {
|
||||||
description: entity.additionalInfo ? entity.additionalInfo.description : ''
|
description: entity.additionalInfo ? entity.additionalInfo.description : ''
|
||||||
}
|
}
|
||||||
@ -108,8 +135,6 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
|
|||||||
if (!this.isAdd && this.entityForm.enabled) {
|
if (!this.isAdd && this.entityForm.enabled) {
|
||||||
this.entityForm.disable({emitEvent: false});
|
this.entityForm.disable({emitEvent: false});
|
||||||
this.entityForm.get('additionalInfo').enable({emitEvent: false});
|
this.entityForm.get('additionalInfo').enable({emitEvent: false});
|
||||||
// this.entityForm.get('dataSize').disable({emitEvent: false});
|
|
||||||
// this.entityForm.get('contentType').disable({emitEvent: false});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,4 +159,21 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
|
|||||||
horizontalPosition: 'right'
|
horizontalPosition: 'right'
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPackageDirectUrlCopied() {
|
||||||
|
this.store.dispatch(new ActionNotificationShow(
|
||||||
|
{
|
||||||
|
message: this.translate.instant('ota-update.checksum-copied-message'),
|
||||||
|
type: 'success',
|
||||||
|
duration: 750,
|
||||||
|
verticalPosition: 'bottom',
|
||||||
|
horizontalPosition: 'right'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareFormValue(formValue: any): any {
|
||||||
|
delete formValue.resource;
|
||||||
|
delete formValue.generateChecksum;
|
||||||
|
return super.prepareFormValue(formValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
<!--
|
||||||
|
|
||||||
|
Copyright © 2016-2021 The Thingsboard Authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<button mat-icon-button
|
||||||
|
[disabled]="disabled"
|
||||||
|
[matTooltip]="matTooltipText"
|
||||||
|
[matTooltipPosition]="matTooltipPosition"
|
||||||
|
(click)="copy($event)">
|
||||||
|
<mat-icon [svgIcon]="mdiIconSymbol" [ngStyle]="style" [ngClass]="{'copied': copied}">
|
||||||
|
{{ iconSymbol }}
|
||||||
|
</mat-icon>
|
||||||
|
</button>
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2021 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
:host {
|
||||||
|
.mat-icon-button{
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
.mat-icon.copied{
|
||||||
|
color: #00C851 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover{
|
||||||
|
.mat-icon{
|
||||||
|
color: #28567E !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
///
|
||||||
|
/// Copyright © 2016-2021 The Thingsboard Authors
|
||||||
|
///
|
||||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
/// you may not use this file except in compliance with the License.
|
||||||
|
/// You may obtain a copy of the License at
|
||||||
|
///
|
||||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
///
|
||||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
/// See the License for the specific language governing permissions and
|
||||||
|
/// limitations under the License.
|
||||||
|
///
|
||||||
|
|
||||||
|
import { ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { ClipboardService } from 'ngx-clipboard';
|
||||||
|
import { TooltipPosition } from '@angular/material/tooltip';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tb-copy-button',
|
||||||
|
styleUrls: ['copy-button.component.scss'],
|
||||||
|
templateUrl: './copy-button.component.html'
|
||||||
|
})
|
||||||
|
export class CopyButtonComponent {
|
||||||
|
|
||||||
|
private copedIcon = '';
|
||||||
|
private timer;
|
||||||
|
|
||||||
|
copied = false;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
copyText: string;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
disabled = false;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
mdiIcon: string;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
icon: string;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
tooltipText: string;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
tooltipPosition: TooltipPosition;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
style: {[key: string]: any} = {};
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
successCopied = new EventEmitter<string>();
|
||||||
|
|
||||||
|
constructor(private clipboardService: ClipboardService,
|
||||||
|
private translate: TranslateService,
|
||||||
|
private cd: ChangeDetectorRef) {
|
||||||
|
}
|
||||||
|
|
||||||
|
copy($event: Event): void {
|
||||||
|
$event.stopPropagation();
|
||||||
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
}
|
||||||
|
this.clipboardService.copy(this.copyText);
|
||||||
|
this.successCopied.emit(this.copyText);
|
||||||
|
this.copedIcon = 'done';
|
||||||
|
this.copied = true;
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
this.copedIcon = null;
|
||||||
|
this.copied = false;
|
||||||
|
this.cd.detectChanges();
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
get iconSymbol(): string {
|
||||||
|
return this.copedIcon || this.icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
get mdiIconSymbol(): string {
|
||||||
|
return this.copedIcon ? '' : this.mdiIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
get matTooltipText(): string {
|
||||||
|
return this.copied ? this.translate.instant('ota-update.copied') : this.tooltipText;
|
||||||
|
}
|
||||||
|
|
||||||
|
get matTooltipPosition(): TooltipPosition {
|
||||||
|
return this.copied ? 'below' : this.tooltipPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -216,7 +216,7 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On
|
|||||||
direction: Direction.ASC
|
direction: Direction.ASC
|
||||||
});
|
});
|
||||||
return this.otaPackageService.getOtaPackagesInfoByDeviceProfileId(pageLink, this.deviceProfileId, this.type,
|
return this.otaPackageService.getOtaPackagesInfoByDeviceProfileId(pageLink, this.deviceProfileId, this.type,
|
||||||
true, {ignoreLoading: true}).pipe(
|
{ignoreLoading: true}).pipe(
|
||||||
map((data) => data && data.data.length ? data.data : null)
|
map((data) => data && data.data.length ? data.data : null)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,6 +87,7 @@ export interface OtaPackageInfo extends BaseData<OtaPackageId> {
|
|||||||
title?: string;
|
title?: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
hasData?: boolean;
|
hasData?: boolean;
|
||||||
|
url?: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
checksum?: string;
|
checksum?: string;
|
||||||
checksumAlgorithm?: ChecksumAlgorithm;
|
checksumAlgorithm?: ChecksumAlgorithm;
|
||||||
|
|||||||
@ -143,6 +143,7 @@ import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
|
|||||||
import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component';
|
import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component';
|
||||||
import { OtaPackageAutocompleteComponent } from '@shared/components/ota-package/ota-package-autocomplete.component';
|
import { OtaPackageAutocompleteComponent } from '@shared/components/ota-package/ota-package-autocomplete.component';
|
||||||
import { MAT_DATE_LOCALE } from '@angular/material/core';
|
import { MAT_DATE_LOCALE } from '@angular/material/core';
|
||||||
|
import { CopyButtonComponent } from '@shared/components/button/copy-button.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
providers: [
|
providers: [
|
||||||
@ -240,7 +241,8 @@ import { MAT_DATE_LOCALE } from '@angular/material/core';
|
|||||||
EntityGatewaySelectComponent,
|
EntityGatewaySelectComponent,
|
||||||
ContactComponent,
|
ContactComponent,
|
||||||
OtaPackageAutocompleteComponent,
|
OtaPackageAutocompleteComponent,
|
||||||
WidgetsBundleSearchComponent
|
WidgetsBundleSearchComponent,
|
||||||
|
CopyButtonComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@ -412,7 +414,8 @@ import { MAT_DATE_LOCALE } from '@angular/material/core';
|
|||||||
EntityGatewaySelectComponent,
|
EntityGatewaySelectComponent,
|
||||||
ContactComponent,
|
ContactComponent,
|
||||||
OtaPackageAutocompleteComponent,
|
OtaPackageAutocompleteComponent,
|
||||||
WidgetsBundleSearchComponent
|
WidgetsBundleSearchComponent,
|
||||||
|
CopyButtonComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SharedModule { }
|
export class SharedModule { }
|
||||||
|
|||||||
@ -575,7 +575,8 @@
|
|||||||
"enter-password": "Enter password",
|
"enter-password": "Enter password",
|
||||||
"enter-search": "Enter search",
|
"enter-search": "Enter search",
|
||||||
"created-time": "Created time",
|
"created-time": "Created time",
|
||||||
"loading": "Loading..."
|
"loading": "Loading...",
|
||||||
|
"proceed": "Proceed"
|
||||||
},
|
},
|
||||||
"content-type": {
|
"content-type": {
|
||||||
"json": "Json",
|
"json": "Json",
|
||||||
@ -2157,21 +2158,30 @@
|
|||||||
"assign-firmware-required": "Assigned firmware is required",
|
"assign-firmware-required": "Assigned firmware is required",
|
||||||
"assign-software": "Assigned software",
|
"assign-software": "Assigned software",
|
||||||
"assign-software-required": "Assigned software is required",
|
"assign-software-required": "Assigned software is required",
|
||||||
|
"auto-generate-checksum": "Auto-generate checksum",
|
||||||
"checksum": "Checksum",
|
"checksum": "Checksum",
|
||||||
|
"checksum-hint": "If checksum is empty, it will be generated automatically",
|
||||||
"checksum-algorithm": "Checksum algorithm",
|
"checksum-algorithm": "Checksum algorithm",
|
||||||
"checksum-copied-message": "Package checksum has been copied to clipboard",
|
"checksum-copied-message": "Package checksum has been copied to clipboard",
|
||||||
"chose-compatible-device-profile": "Choose compatible device profile",
|
"change-firmware": "Change of the firmware may cause update of { count, plural, 1 {1 device} other {# devices} }.",
|
||||||
|
"change-software": "Change of the software may cause update of { count, plural, 1 {1 device} other {# devices} }.",
|
||||||
|
"chose-compatible-device-profile": "The uploaded package will be available only for devices with the chosen profile.",
|
||||||
"chose-firmware-distributed-device": "Choose firmware that will be distributed to the devices",
|
"chose-firmware-distributed-device": "Choose firmware that will be distributed to the devices",
|
||||||
"chose-software-distributed-device": "Choose software that will be distributed to the devices",
|
"chose-software-distributed-device": "Choose software that will be distributed to the devices",
|
||||||
"content-type": "Content type",
|
"content-type": "Content type",
|
||||||
"copy-checksum": "Copy checksum",
|
"copy-checksum": "Copy checksum",
|
||||||
|
"copy-direct-url": "Copy direct URL",
|
||||||
"copyId": "Copy package Id",
|
"copyId": "Copy package Id",
|
||||||
|
"copied": "Copied!",
|
||||||
"delete": "Delete package",
|
"delete": "Delete package",
|
||||||
"delete-ota-update-text": "Be careful, after the confirmation the OTA update will become unrecoverable.",
|
"delete-ota-update-text": "Be careful, after the confirmation the OTA update will become unrecoverable.",
|
||||||
"delete-ota-update-title": "Are you sure you want to delete the OTA update '{{title}}'?",
|
"delete-ota-update-title": "Are you sure you want to delete the OTA update '{{title}}'?",
|
||||||
"delete-ota-updates-text": "Be careful, after the confirmation all selected OTA updates will be removed.",
|
"delete-ota-updates-text": "Be careful, after the confirmation all selected OTA updates will be removed.",
|
||||||
"delete-ota-updates-title": "Are you sure you want to delete { count, plural, 1 {1 OTA update} other {# OTA updates} }?",
|
"delete-ota-updates-title": "Are you sure you want to delete { count, plural, 1 {1 OTA update} other {# OTA updates} }?",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
|
"direct-url": "Direct URL",
|
||||||
|
"direct-url-copied-message": "Package direct URL has been copied to clipboard",
|
||||||
|
"direct-url-required": "Direct URL is required",
|
||||||
"download": "Download package",
|
"download": "Download package",
|
||||||
"drop-file": "Drop a package file or click to select a file to upload.",
|
"drop-file": "Drop a package file or click to select a file to upload.",
|
||||||
"file-name": "File name",
|
"file-name": "File name",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user