Showing
13 changed files
with
256 additions
and
40 deletions
... | ... | @@ -39,7 +39,7 @@ export class OtaPackageService { |
39 | 39 | } |
40 | 40 | |
41 | 41 | public getOtaPackagesInfoByDeviceProfileId(pageLink: PageLink, deviceProfileId: string, type: OtaUpdateType, |
42 | - hasData = true, config?: RequestConfig): Observable<PageData<OtaPackageInfo>> { | |
42 | + config?: RequestConfig): Observable<PageData<OtaPackageInfo>> { | |
43 | 43 | const url = `/api/otaPackages/${deviceProfileId}/${type}${pageLink.toQuery()}`; |
44 | 44 | return this.http.get<PageData<OtaPackageInfo>>(url, defaultHttpOptionsFromConfig(config)); |
45 | 45 | } | ... | ... |
... | ... | @@ -163,8 +163,34 @@ |
163 | 163 | *matCellDef="let entity; let row = index" |
164 | 164 | [matTooltip]="cellTooltip(entity, column, row)" |
165 | 165 | matTooltipPosition="above" |
166 | - [innerHTML]="cellContent(entity, column, row)" | |
167 | 166 | [ngStyle]="cellStyle(entity, column, row)"> |
167 | + <span [innerHTML]="cellContent(entity, column, row)"></span> | |
168 | + <ng-template [ngIf]="column.actionCell.name"> | |
169 | + <ng-container [ngSwitch]="column.actionCell.type"> | |
170 | + <ng-template [ngSwitchCase]="cellActionType.COPY_BUTTON"> | |
171 | + <tb-copy-button | |
172 | + [disabled]="isLoading$ | async" | |
173 | + [fxShow]="column.actionCell.isEnabled(entity)" | |
174 | + [copyText]="column.actionCell.onAction(null, entity)" | |
175 | + tooltipText="{{ column.actionCell.nameFunction ? column.actionCell.nameFunction(entity) : column.actionCell.name }}" | |
176 | + tooltipPosition="above" | |
177 | + [icon]="column.actionCell.icon" | |
178 | + [mdiIcon]="column.actionCell.mdiIcon" [style]="column.actionCell.style"> | |
179 | + </tb-copy-button> | |
180 | + </ng-template> | |
181 | + <ng-template ngSwitchDefault> | |
182 | + <button mat-icon-button [disabled]="isLoading$ | async" | |
183 | + [fxShow]="column.actionCell.isEnabled(entity)" | |
184 | + matTooltip="{{ column.actionCell.nameFunction ? column.actionCell.nameFunction(entity) : column.actionCell.name }}" | |
185 | + matTooltipPosition="above" | |
186 | + (click)="column.actionCell.onAction($event, entity)"> | |
187 | + <mat-icon [svgIcon]="column.actionCell.mdiIcon" [ngStyle]="column.actionCell.style"> | |
188 | + {{column.actionCell.icon}} | |
189 | + </mat-icon> | |
190 | + </button> | |
191 | + </ng-template> | |
192 | + </ng-container> | |
193 | + </ng-template> | |
168 | 194 | </mat-cell> |
169 | 195 | </ng-container> |
170 | 196 | <ng-container [matColumnDef]="column.key" *ngFor="let column of actionColumns; trackBy: trackByColumnKey;"> | ... | ... |
... | ... | @@ -43,7 +43,7 @@ import { TranslateService } from '@ngx-translate/core'; |
43 | 43 | import { BaseData, HasId } from '@shared/models/base-data'; |
44 | 44 | import { ActivatedRoute } from '@angular/router'; |
45 | 45 | import { |
46 | - CellActionDescriptor, | |
46 | + CellActionDescriptor, CellActionDescriptorType, | |
47 | 47 | EntityActionTableColumn, |
48 | 48 | EntityColumn, |
49 | 49 | EntityTableColumn, |
... | ... | @@ -104,6 +104,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn |
104 | 104 | timewindow: Timewindow; |
105 | 105 | dataSource: EntitiesDataSource<BaseData<HasId>>; |
106 | 106 | |
107 | + cellActionType = CellActionDescriptorType; | |
108 | + | |
107 | 109 | isDetailsOpen = false; |
108 | 110 | detailsPanelOpened = new EventEmitter<boolean>(); |
109 | 111 | ... | ... |
... | ... | @@ -48,6 +48,9 @@ export type CellContentFunction<T extends BaseData<HasId>> = (entity: T, key: st |
48 | 48 | export type CellTooltipFunction<T extends BaseData<HasId>> = (entity: T, key: string) => string | undefined; |
49 | 49 | export type HeaderCellStyleFunction<T extends BaseData<HasId>> = (key: string) => object; |
50 | 50 | export type CellStyleFunction<T extends BaseData<HasId>> = (entity: T, key: string) => object; |
51 | +export type CopyCellContent<T extends BaseData<HasId>> = (entity: T, key: string, length: number) => object; | |
52 | + | |
53 | +export enum CellActionDescriptorType { 'DEFAULT', 'COPY_BUTTON'} | |
51 | 54 | |
52 | 55 | export interface CellActionDescriptor<T extends BaseData<HasId>> { |
53 | 56 | name: string; |
... | ... | @@ -56,7 +59,8 @@ export interface CellActionDescriptor<T extends BaseData<HasId>> { |
56 | 59 | mdiIcon?: string; |
57 | 60 | style?: any; |
58 | 61 | isEnabled: (entity: T) => boolean; |
59 | - onAction: ($event: MouseEvent, entity: T) => void; | |
62 | + onAction: ($event: MouseEvent, entity: T) => any; | |
63 | + type?: CellActionDescriptorType; | |
60 | 64 | } |
61 | 65 | |
62 | 66 | export interface GroupActionDescriptor<T extends BaseData<HasId>> { |
... | ... | @@ -95,7 +99,8 @@ export class EntityTableColumn<T extends BaseData<HasId>> extends BaseEntityTabl |
95 | 99 | public sortable: boolean = true, |
96 | 100 | public headerCellStyleFunction: HeaderCellStyleFunction<T> = () => ({}), |
97 | 101 | public cellTooltipFunction: CellTooltipFunction<T> = () => undefined, |
98 | - public isNumberColumn: boolean = false) { | |
102 | + public isNumberColumn: boolean = false, | |
103 | + public actionCell: CellActionDescriptor<T> = {isEnabled: () => false, name: null, onAction: () => ({})}) { | |
99 | 104 | super('content', key, title, width, sortable); |
100 | 105 | } |
101 | 106 | } |
... | ... | @@ -187,3 +192,7 @@ export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = P |
187 | 192 | export function checkBoxCell(value: boolean): string { |
188 | 193 | return `<mat-icon class="material-icons mat-icon">${value ? 'check_box' : 'check_box_outline_blank'}</mat-icon>`; |
189 | 194 | } |
195 | + | |
196 | +export function copyCellContent(value: string): string { | |
197 | + return `<mat-icon class="material-icons mat-icon">${value ? 'check_box' : 'check_box_outline_blank'}</mat-icon>`; | |
198 | +} | ... | ... |
... | ... | @@ -17,6 +17,7 @@ |
17 | 17 | import { Injectable } from '@angular/core'; |
18 | 18 | import { Resolve } from '@angular/router'; |
19 | 19 | import { |
20 | + CellActionDescriptorType, | |
20 | 21 | DateEntityTableColumn, |
21 | 22 | EntityTableColumn, |
22 | 23 | EntityTableConfig |
... | ... | @@ -61,27 +62,46 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot |
61 | 62 | |
62 | 63 | this.config.columns.push( |
63 | 64 | new DateEntityTableColumn<OtaPackageInfo>('createdTime', 'common.created-time', this.datePipe, '150px'), |
64 | - new EntityTableColumn<OtaPackageInfo>('title', 'ota-update.title', '25%'), | |
65 | - new EntityTableColumn<OtaPackageInfo>('version', 'ota-update.version', '25%'), | |
66 | - new EntityTableColumn<OtaPackageInfo>('type', 'ota-update.package-type', '25%', entity => { | |
65 | + new EntityTableColumn<OtaPackageInfo>('title', 'ota-update.title', '20%'), | |
66 | + new EntityTableColumn<OtaPackageInfo>('version', 'ota-update.version', '20%'), | |
67 | + new EntityTableColumn<OtaPackageInfo>('type', 'ota-update.package-type', '20%', entity => { | |
67 | 68 | return this.translate.instant(OtaUpdateTypeTranslationMap.get(entity.type)); |
68 | 69 | }), |
69 | - new EntityTableColumn<OtaPackageInfo>('fileName', 'ota-update.file-name', '25%'), | |
70 | + new EntityTableColumn<OtaPackageInfo>('url', 'ota-update.url', '20%', entity => { | |
71 | + return entity.url && entity.url.length > 20 ? `${entity.url.slice(0, 20)}…` : ''; | |
72 | + }, () => ({}), true, () => ({}), () => undefined, false, | |
73 | + { | |
74 | + name: this.translate.instant('ota-update.copy-checksum'), | |
75 | + icon: 'content_paste', | |
76 | + style: { | |
77 | + 'font-size': '16px', | |
78 | + color: 'rgba(0,0,0,.87)' | |
79 | + }, | |
80 | + isEnabled: (otaPackage) => !!otaPackage.url, | |
81 | + onAction: ($event, entity) => entity.url, | |
82 | + type: CellActionDescriptorType.COPY_BUTTON | |
83 | + }), | |
84 | + new EntityTableColumn<OtaPackageInfo>('fileName', 'ota-update.file-name', '20%'), | |
70 | 85 | new EntityTableColumn<OtaPackageInfo>('dataSize', 'ota-update.file-size', '70px', entity => { |
71 | 86 | return entity.dataSize ? this.fileSize.transform(entity.dataSize) : ''; |
72 | 87 | }), |
73 | - new EntityTableColumn<OtaPackageInfo>('checksum', 'ota-update.checksum', '540px', entity => { | |
74 | - return entity.checksum ? `${ChecksumAlgorithmTranslationMap.get(entity.checksumAlgorithm)}: ${entity.checksum}` : ''; | |
75 | - }, () => ({}), false) | |
76 | - ); | |
77 | - | |
78 | - this.config.cellActionDescriptors.push( | |
88 | + new EntityTableColumn<OtaPackageInfo>('checksum', 'ota-update.checksum', '220px', entity => { | |
89 | + return entity.checksum ? this.checksumText(entity) : ''; | |
90 | + }, () => ({}), true, () => ({}), () => undefined, false, | |
79 | 91 | { |
80 | 92 | name: this.translate.instant('ota-update.copy-checksum'), |
81 | - icon: 'content_copy', | |
93 | + icon: 'content_paste', | |
94 | + style: { | |
95 | + 'font-size': '16px', | |
96 | + color: 'rgba(0,0,0,.87)' | |
97 | + }, | |
82 | 98 | isEnabled: (otaPackage) => !!otaPackage.checksum, |
83 | - onAction: ($event, entity) => this.copyPackageChecksum($event, entity) | |
84 | - }, | |
99 | + onAction: ($event, entity) => entity.checksum, | |
100 | + type: CellActionDescriptorType.COPY_BUTTON | |
101 | + }) | |
102 | + ); | |
103 | + | |
104 | + this.config.cellActionDescriptors.push( | |
85 | 105 | { |
86 | 106 | name: this.translate.instant('ota-update.download'), |
87 | 107 | icon: 'file_download', |
... | ... | @@ -120,19 +140,12 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot |
120 | 140 | } |
121 | 141 | } |
122 | 142 | |
123 | - copyPackageChecksum($event: Event, otaPackageInfo: OtaPackageInfo) { | |
124 | - if ($event) { | |
125 | - $event.stopPropagation(); | |
143 | + checksumText(entity): string { | |
144 | + let text = `${ChecksumAlgorithmTranslationMap.get(entity.checksumAlgorithm)}: ${entity.checksum}`; | |
145 | + if (text.length > 20) { | |
146 | + text = `${text.slice(0, 20)}…`; | |
126 | 147 | } |
127 | - this.clipboardService.copy(otaPackageInfo?.checksum); | |
128 | - this.store.dispatch(new ActionNotificationShow( | |
129 | - { | |
130 | - message: this.translate.instant('ota-update.checksum-copied-message'), | |
131 | - type: 'success', | |
132 | - duration: 750, | |
133 | - verticalPosition: 'bottom', | |
134 | - horizontalPosition: 'right' | |
135 | - })); | |
148 | + return text; | |
136 | 149 | } |
137 | 150 | |
138 | 151 | onPackageAction(action: EntityAction<OtaPackageInfo>): boolean { |
... | ... | @@ -140,9 +153,6 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot |
140 | 153 | case 'uploadPackage': |
141 | 154 | this.exportPackage(action.event, action.entity); |
142 | 155 | return true; |
143 | - case 'copyChecksum': | |
144 | - this.copyPackageChecksum(action.event, action.entity); | |
145 | - return true; | |
146 | 156 | } |
147 | 157 | return false; |
148 | 158 | } | ... | ... |
... | ... | @@ -31,7 +31,6 @@ |
31 | 31 | <div fxLayout="row" fxLayout.xs="column"> |
32 | 32 | <button mat-raised-button |
33 | 33 | ngxClipboard |
34 | - [disabled]="(isLoading$ | async)" | |
35 | 34 | (cbOnSuccess)="onPackageIdCopied()" |
36 | 35 | [cbContent]="entity?.id?.id" |
37 | 36 | [fxShow]="!isEdit"> |
... | ... | @@ -39,9 +38,10 @@ |
39 | 38 | <span translate>ota-update.copyId</span> |
40 | 39 | </button> |
41 | 40 | <button mat-raised-button |
42 | - [disabled]="(isLoading$ | async)" | |
43 | - (click)="onEntityAction($event, 'copyChecksum')" | |
44 | - [fxShow]="!isEdit && entity?.checksum"> | |
41 | + ngxClipboard | |
42 | + (cbOnSuccess)="onPackageChecksumCopied()" | |
43 | + [cbContent]="entity?.checksum" | |
44 | + [fxShow]="!isEdit"> | |
45 | 45 | <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> |
46 | 46 | <span translate>ota-update.copy-checksum</span> |
47 | 47 | </button> | ... | ... |
... | ... | @@ -149,6 +149,17 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O |
149 | 149 | })); |
150 | 150 | } |
151 | 151 | |
152 | + onPackageChecksumCopied() { | |
153 | + this.store.dispatch(new ActionNotificationShow( | |
154 | + { | |
155 | + message: this.translate.instant('ota-update.checksum-copied-message'), | |
156 | + type: 'success', | |
157 | + duration: 750, | |
158 | + verticalPosition: 'bottom', | |
159 | + horizontalPosition: 'right' | |
160 | + })); | |
161 | + } | |
162 | + | |
152 | 163 | prepareFormValue(formValue: any): any { |
153 | 164 | delete formValue.resource; |
154 | 165 | delete formValue.generateChecksum; | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2021 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<button mat-icon-button | |
19 | + #tooltip | |
20 | + [disabled]="disabled" | |
21 | + [matTooltip]="matTooltipText" | |
22 | + [matTooltipPosition]="matTooltipPosition" | |
23 | + (mouseenter)="copied ? $event.stopImmediatePropagation():''" | |
24 | + (mouseleave)="copied ? $event.stopImmediatePropagation():''" | |
25 | + (click)="copy($event)"> | |
26 | + <mat-icon [svgIcon]="mdiIconSymbol" [ngStyle]="style" [ngClass]="{'copied': copied}"> | |
27 | + {{ iconSymbol }} | |
28 | + </mat-icon> | |
29 | +</button> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2021 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +:host { | |
17 | + .mat-icon-button{ | |
18 | + height: 32px; | |
19 | + width: 32px; | |
20 | + line-height: 32px; | |
21 | + .mat-icon.copied{ | |
22 | + color: #00C851!important; | |
23 | + } | |
24 | + } | |
25 | + &:hover{ | |
26 | + .mat-icon{ | |
27 | + color: #28567E !important; | |
28 | + } | |
29 | + } | |
30 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2021 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { ChangeDetectorRef, Component, Input, ViewChild } from '@angular/core'; | |
18 | +import { ClipboardService } from 'ngx-clipboard'; | |
19 | +import { MatTooltip, TooltipPosition } from '@angular/material/tooltip/tooltip'; | |
20 | +import { TranslateService } from '@ngx-translate/core'; | |
21 | + | |
22 | +@Component({ | |
23 | + selector: 'tb-copy-button', | |
24 | + styleUrls: ['copy-button.component.scss'], | |
25 | + templateUrl: './copy-button.component.html' | |
26 | +}) | |
27 | +export class CopyButtonComponent { | |
28 | + | |
29 | + private copedIcon = ''; | |
30 | + private timer; | |
31 | + | |
32 | + copied = false; | |
33 | + | |
34 | + @Input() | |
35 | + copyText: string; | |
36 | + | |
37 | + @Input() | |
38 | + disabled = false; | |
39 | + | |
40 | + @Input() | |
41 | + mdiIcon: string; | |
42 | + | |
43 | + @Input() | |
44 | + icon: string; | |
45 | + | |
46 | + @Input() | |
47 | + tooltipText: string; | |
48 | + | |
49 | + @Input() | |
50 | + tooltipPosition: TooltipPosition; | |
51 | + | |
52 | + @Input() | |
53 | + style: {[key: string]: any} = {}; | |
54 | + | |
55 | + constructor(private clipboardService: ClipboardService, | |
56 | + private translate: TranslateService, | |
57 | + private cd: ChangeDetectorRef) { | |
58 | + } | |
59 | + | |
60 | + @ViewChild('tooltip', {static: true}) tooltip: MatTooltip; | |
61 | + | |
62 | + copy($event: Event): void { | |
63 | + $event.stopPropagation(); | |
64 | + $event.preventDefault(); | |
65 | + if (this.timer) { | |
66 | + clearTimeout(this.timer); | |
67 | + } | |
68 | + this.clipboardService.copy(this.copyText); | |
69 | + this.copedIcon = 'done'; | |
70 | + this.copied = true; | |
71 | + this.tooltip.show(); | |
72 | + this.timer = setTimeout(() => { | |
73 | + this.copedIcon = null; | |
74 | + this.copied = false; | |
75 | + this.tooltip.hide(); | |
76 | + this.cd.detectChanges(); | |
77 | + }, 1500); | |
78 | + } | |
79 | + | |
80 | + get iconSymbol(): string { | |
81 | + return this.copedIcon || this.icon; | |
82 | + } | |
83 | + | |
84 | + get mdiIconSymbol(): string { | |
85 | + return this.copedIcon ? '' : this.mdiIcon; | |
86 | + } | |
87 | + | |
88 | + get matTooltipText(): string { | |
89 | + return this.copied ? this.translate.instant('ota-update.copied') : this.tooltipText; | |
90 | + } | |
91 | + | |
92 | + get matTooltipPosition(): TooltipPosition { | |
93 | + return this.copied ? 'below' : this.tooltipPosition; | |
94 | + } | |
95 | +} | ... | ... |
... | ... | @@ -216,7 +216,7 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On |
216 | 216 | direction: Direction.ASC |
217 | 217 | }); |
218 | 218 | return this.otaPackageService.getOtaPackagesInfoByDeviceProfileId(pageLink, this.deviceProfileId, this.type, |
219 | - true, {ignoreLoading: true}).pipe( | |
219 | + {ignoreLoading: true}).pipe( | |
220 | 220 | map((data) => data && data.data.length ? data.data : null) |
221 | 221 | ); |
222 | 222 | } | ... | ... |
... | ... | @@ -143,6 +143,7 @@ import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe'; |
143 | 143 | import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component'; |
144 | 144 | import { OtaPackageAutocompleteComponent } from '@shared/components/ota-package/ota-package-autocomplete.component'; |
145 | 145 | import { MAT_DATE_LOCALE } from '@angular/material/core'; |
146 | +import { CopyButtonComponent } from '@shared/components/button/copy-button.component'; | |
146 | 147 | |
147 | 148 | @NgModule({ |
148 | 149 | providers: [ |
... | ... | @@ -240,7 +241,8 @@ import { MAT_DATE_LOCALE } from '@angular/material/core'; |
240 | 241 | EntityGatewaySelectComponent, |
241 | 242 | ContactComponent, |
242 | 243 | OtaPackageAutocompleteComponent, |
243 | - WidgetsBundleSearchComponent | |
244 | + WidgetsBundleSearchComponent, | |
245 | + CopyButtonComponent | |
244 | 246 | ], |
245 | 247 | imports: [ |
246 | 248 | CommonModule, |
... | ... | @@ -412,7 +414,8 @@ import { MAT_DATE_LOCALE } from '@angular/material/core'; |
412 | 414 | EntityGatewaySelectComponent, |
413 | 415 | ContactComponent, |
414 | 416 | OtaPackageAutocompleteComponent, |
415 | - WidgetsBundleSearchComponent | |
417 | + WidgetsBundleSearchComponent, | |
418 | + CopyButtonComponent | |
416 | 419 | ] |
417 | 420 | }) |
418 | 421 | export class SharedModule { } | ... | ... |
... | ... | @@ -2166,6 +2166,7 @@ |
2166 | 2166 | "content-type": "Content type", |
2167 | 2167 | "copy-checksum": "Copy checksum", |
2168 | 2168 | "copyId": "Copy package Id", |
2169 | + "copied": "Copied!", | |
2169 | 2170 | "delete": "Delete package", |
2170 | 2171 | "delete-ota-update-text": "Be careful, after the confirmation the OTA update will become unrecoverable.", |
2171 | 2172 | "delete-ota-update-title": "Are you sure you want to delete the OTA update '{{title}}'?", | ... | ... |