Commit 064778951a76cf718ad8478d17413ccba166db41

Authored by Igor Kulikov
1 parent c39fb8c9

UI: Ability to update dashboard image from screenshot

@@ -47,6 +47,7 @@ @@ -47,6 +47,7 @@
47 "flot": "git://github.com/thingsboard/flot.git#0.9-work", 47 "flot": "git://github.com/thingsboard/flot.git#0.9-work",
48 "flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master", 48 "flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master",
49 "font-awesome": "^4.7.0", 49 "font-awesome": "^4.7.0",
  50 + "html2canvas": "^1.0.0-rc.7",
50 "jquery": "^3.5.1", 51 "jquery": "^3.5.1",
51 "jquery.terminal": "^2.18.3", 52 "jquery.terminal": "^2.18.3",
52 "js-beautify": "^1.13.0", 53 "js-beautify": "^1.13.0",
  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 +<form (ngSubmit)="save()">
  19 + <mat-toolbar color="primary">
  20 + <h2 translate>dashboard.update-image</h2>
  21 + <span fxFlex></span>
  22 + <button mat-button mat-icon-button
  23 + (click)="cancel()"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div mat-dialog-content style="position: relative;">
  31 + <fieldset [disabled]="(isLoading$ | async) || (takingScreenshot$ | async)" fxLayout="column" fxLayoutAlign="center center" fxLayoutGap="16px">
  32 + <div class="tb-image-preview-container">
  33 + <div *ngIf="!safeImageUrl; else elseBlock">{{ 'dashboard.no-image' | translate }}</div>
  34 + <ng-template #elseBlock><img class="tb-image-preview" [src]="safeImageUrl" /></ng-template>
  35 + </div>
  36 + <button mat-raised-button color="accent"
  37 + type="button"
  38 + [disabled]="(isLoading$ | async) || (takingScreenshot$ | async)"
  39 + (click)="takeScreenShot()">
  40 + {{ 'dashboard.take-screenshot' | translate }}
  41 + </button>
  42 + <div [formGroup]="dashboardImageFormGroup">
  43 + <tb-image-input [showPreview]="false" label="{{'dashboard.image' | translate}}"
  44 + formControlName="dashboardImage">
  45 + </tb-image-input>
  46 + </div>
  47 + </fieldset>
  48 + <div *ngIf="takingScreenshot$ | async" class="taking-screenshot-progress tb-absolute-fill" fxLayout="column"
  49 + fxLayoutAlign="center center">
  50 + <mat-progress-spinner color="accent" mode="indeterminate"></mat-progress-spinner>
  51 + </div>
  52 + </div>
  53 + <div mat-dialog-actions fxLayoutAlign="end center">
  54 + <button mat-button color="primary"
  55 + type="button"
  56 + [disabled]="(isLoading$ | async) || (takingScreenshot$ | async)"
  57 + (click)="cancel()" cdkFocusInitial>
  58 + {{ 'action.cancel' | translate }}
  59 + </button>
  60 + <button mat-raised-button color="primary"
  61 + type="submit"
  62 + [disabled]="(isLoading$ | async) || (takingScreenshot$ | async) || !dashboardImageFormGroup.dirty">
  63 + {{ 'action.update' | translate }}
  64 + </button>
  65 + </div>
  66 +</form>
  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 +$previewSize: 300px !default;
  18 +
  19 +:host {
  20 + .tb-image-preview {
  21 + width: auto;
  22 + max-width: $previewSize - 2;
  23 + height: auto;
  24 + max-height: $previewSize - 2;
  25 + }
  26 +
  27 + .tb-image-preview-container {
  28 + position: relative;
  29 + float: left;
  30 + width: $previewSize;
  31 + height: $previewSize;
  32 + margin-right: 12px;
  33 + vertical-align: top;
  34 + border: solid 1px;
  35 +
  36 + div {
  37 + width: 100%;
  38 + font-size: 18px;
  39 + text-align: center;
  40 + }
  41 +
  42 + div,
  43 + .tb-image-preview {
  44 + position: absolute;
  45 + top: 50%;
  46 + left: 50%;
  47 + transform: translate(-50%, -50%);
  48 + }
  49 + }
  50 +
  51 + .taking-screenshot-progress {
  52 + background-color: rgba(255, 255, 255, 0.65);
  53 + }
  54 +}
  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 { Component, Inject } from '@angular/core';
  18 +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { FormBuilder, FormGroup } from '@angular/forms';
  22 +import { Router } from '@angular/router';
  23 +import { DialogComponent } from '@app/shared/components/dialog.component';
  24 +import { DashboardId } from '@shared/models/id/dashboard-id';
  25 +import { DashboardService } from '@core/http/dashboard.service';
  26 +import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
  27 +import html2canvas from 'html2canvas';
  28 +import { map, share } from 'rxjs/operators';
  29 +import { BehaviorSubject, from } from 'rxjs';
  30 +
  31 +export interface DashboardImageDialogData {
  32 + dashboardId: DashboardId;
  33 + currentImage?: string;
  34 + dashboardElement: HTMLElement;
  35 +}
  36 +
  37 +export interface DashboardImageDialogResult {
  38 + image?: string;
  39 +}
  40 +
  41 +@Component({
  42 + selector: 'tb-dashboard-image-dialog',
  43 + templateUrl: './dashboard-image-dialog.component.html',
  44 + styleUrls: ['./dashboard-image-dialog.component.scss']
  45 +})
  46 +export class DashboardImageDialogComponent extends DialogComponent<DashboardImageDialogComponent, DashboardImageDialogResult> {
  47 +
  48 + takingScreenshotSubject = new BehaviorSubject(false);
  49 +
  50 + takingScreenshot$ = this.takingScreenshotSubject.asObservable().pipe(
  51 + share()
  52 + );
  53 +
  54 + dashboardId: DashboardId;
  55 + safeImageUrl?: SafeUrl;
  56 + dashboardElement: HTMLElement;
  57 +
  58 + dashboardImageFormGroup: FormGroup;
  59 +
  60 + constructor(protected store: Store<AppState>,
  61 + protected router: Router,
  62 + @Inject(MAT_DIALOG_DATA) public data: DashboardImageDialogData,
  63 + public dialogRef: MatDialogRef<DashboardImageDialogComponent, DashboardImageDialogResult>,
  64 + private dashboardService: DashboardService,
  65 + private sanitizer: DomSanitizer,
  66 + private fb: FormBuilder) {
  67 + super(store, router, dialogRef);
  68 +
  69 + this.dashboardId = this.data.dashboardId;
  70 + this.updateImage(this.data.currentImage);
  71 + this.dashboardElement = this.data.dashboardElement;
  72 +
  73 + this.dashboardImageFormGroup = this.fb.group({
  74 + dashboardImage: [this.data.currentImage]
  75 + });
  76 +
  77 + this.dashboardImageFormGroup.get('dashboardImage').valueChanges.subscribe(
  78 + (newImage) => {
  79 + this.updateImage(newImage);
  80 + }
  81 + );
  82 + }
  83 +
  84 + takeScreenShot() {
  85 + this.takingScreenshotSubject.next(true);
  86 + from(html2canvas(this.dashboardElement, {
  87 + logging: false,
  88 + useCORS: true,
  89 + foreignObjectRendering: false,
  90 + scale: 512 / this.dashboardElement.clientWidth
  91 + })).pipe(
  92 + map(canvas => canvas.toDataURL())).subscribe(
  93 + (image) => {
  94 + this.updateImage(image);
  95 + this.dashboardImageFormGroup.patchValue({dashboardImage: image}, {emitEvent: false});
  96 + this.dashboardImageFormGroup.markAsDirty();
  97 + this.takingScreenshotSubject.next(false);
  98 + },
  99 + (e) => {
  100 + this.takingScreenshotSubject.next(false);
  101 + }
  102 + );
  103 + }
  104 +
  105 + cancel(): void {
  106 + this.dialogRef.close(null);
  107 + }
  108 +
  109 + save(): void {
  110 + this.dashboardService.getDashboard(this.dashboardId.id).subscribe(
  111 + (dashboard) => {
  112 + const newImage: string = this.dashboardImageFormGroup.get('dashboardImage').value;
  113 + dashboard.image = newImage;
  114 + this.dashboardService.saveDashboard(dashboard).subscribe(
  115 + () => {
  116 + this.dialogRef.close({
  117 + image: newImage
  118 + });
  119 + }
  120 + );
  121 + }
  122 + );
  123 + }
  124 +
  125 + private updateImage(imageUrl: string) {
  126 + if (imageUrl) {
  127 + this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(imageUrl);
  128 + } else {
  129 + this.safeImageUrl = null;
  130 + }
  131 + }
  132 +}
@@ -81,6 +81,12 @@ @@ -81,6 +81,12 @@
81 (click)="isFullscreen = !isFullscreen"> 81 (click)="isFullscreen = !isFullscreen">
82 <mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> 82 <mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
83 </button> 83 </button>
  84 + <button [fxShow]="currentDashboardId && !isEdit && isTenantAdmin() && displayUpdateDashboardImage()" mat-icon-button
  85 + matTooltip="{{'dashboard.update-image' | translate}}"
  86 + matTooltipPosition="below"
  87 + (click)="updateDashboardImage($event)">
  88 + <mat-icon>wallpaper</mat-icon>
  89 + </button>
84 <button [fxShow]="currentDashboardId && (isEdit || displayExport())" mat-icon-button 90 <button [fxShow]="currentDashboardId && (isEdit || displayExport())" mat-icon-button
85 matTooltip="{{'dashboard.export' | translate}}" 91 matTooltip="{{'dashboard.export' | translate}}"
86 matTooltipPosition="below" 92 matTooltipPosition="below"
@@ -133,6 +139,7 @@ @@ -133,6 +139,7 @@
133 </tb-dashboard-toolbar> 139 </tb-dashboard-toolbar>
134 </section> 140 </section>
135 <section class="tb-dashboard-container tb-absolute-fill" 141 <section class="tb-dashboard-container tb-absolute-fill"
  142 + #dashboardContainer
136 [ngClass]="{ 'is-fullscreen': forceFullscreen, 143 [ngClass]="{ 'is-fullscreen': forceFullscreen,
137 'tb-dashboard-toolbar-opened': toolbarOpened, 144 'tb-dashboard-toolbar-opened': toolbarOpened,
138 'tb-dashboard-toolbar-animated': isToolbarOpenedAnimate, 145 'tb-dashboard-toolbar-animated': isToolbarOpenedAnimate,
@@ -187,7 +194,7 @@ @@ -187,7 +194,7 @@
187 </tb-dashboard-layout> 194 </tb-dashboard-layout>
188 </mat-drawer-content> 195 </mat-drawer-content>
189 </mat-drawer-container> 196 </mat-drawer-container>
190 - <section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end"> 197 + <section data-html2canvas-ignore fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end">
191 <tb-footer-fab-buttons *ngIf="!embedded && !isMobileApp" 198 <tb-footer-fab-buttons *ngIf="!embedded && !isMobileApp"
192 [fxShow]="!isAddingWidget && isEdit && !widgetEditMode" 199 [fxShow]="!isAddingWidget && isEdit && !widgetEditMode"
193 relative 200 relative
@@ -212,7 +219,7 @@ @@ -212,7 +219,7 @@
212 <mat-icon>{{ isEdit ? 'close' : 'edit' }}</mat-icon> 219 <mat-icon>{{ isEdit ? 'close' : 'edit' }}</mat-icon>
213 </button> 220 </button>
214 </section> 221 </section>
215 - <section class="tb-powered-by-footer" [ngStyle]="{'color': dashboard.configuration.settings.titleColor}"> 222 + <section data-html2canvas-ignore class="tb-powered-by-footer" [ngStyle]="{'color': dashboard.configuration.settings.titleColor}">
216 <span>Powered by <a href="https://thingsboard.io" target="_blank">Thingsboard v.{{ thingsboardVersion }}</a></span> 223 <span>Powered by <a href="https://thingsboard.io" target="_blank">Thingsboard v.{{ thingsboardVersion }}</a></span>
217 </section> 224 </section>
218 </mat-drawer-content> 225 </mat-drawer-content>
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 16
17 import { 17 import {
18 ChangeDetectorRef, 18 ChangeDetectorRef,
19 - Component, 19 + Component, ElementRef,
20 Inject, 20 Inject,
21 Injector, 21 Injector,
22 Input, 22 Input,
@@ -123,6 +123,11 @@ import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/ @@ -123,6 +123,11 @@ import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/
123 import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; 123 import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
124 import { MobileService } from '@core/services/mobile.service'; 124 import { MobileService } from '@core/services/mobile.service';
125 125
  126 +import {
  127 + DashboardImageDialogComponent,
  128 + DashboardImageDialogData, DashboardImageDialogResult
  129 +} from '@home/components/dashboard-page/dashboard-image-dialog.component';
  130 +
126 // @dynamic 131 // @dynamic
127 @Component({ 132 @Component({
128 selector: 'tb-dashboard-page', 133 selector: 'tb-dashboard-page',
@@ -153,6 +158,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -153,6 +158,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
153 dashboard: Dashboard; 158 dashboard: Dashboard;
154 dashboardConfiguration: DashboardConfiguration; 159 dashboardConfiguration: DashboardConfiguration;
155 160
  161 + @ViewChild('dashboardContainer') dashboardContainer: ElementRef<HTMLElement>;
  162 +
156 prevDashboard: Dashboard; 163 prevDashboard: Dashboard;
157 164
158 iframeMode = this.utils.iframeMode; 165 iframeMode = this.utils.iframeMode;
@@ -468,6 +475,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -468,6 +475,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
468 } 475 }
469 } 476 }
470 477
  478 + public displayUpdateDashboardImage(): boolean {
  479 + if (this.dashboard.configuration.settings &&
  480 + isDefined(this.dashboard.configuration.settings.showUpdateDashboardImage)) {
  481 + return this.dashboard.configuration.settings.showUpdateDashboardImage;
  482 + } else {
  483 + return true;
  484 + }
  485 + }
  486 +
471 public displayDashboardTimewindow(): boolean { 487 public displayDashboardTimewindow(): boolean {
472 if (this.dashboard.configuration.settings && 488 if (this.dashboard.configuration.settings &&
473 isDefined(this.dashboard.configuration.settings.showDashboardTimewindow)) { 489 isDefined(this.dashboard.configuration.settings.showDashboardTimewindow)) {
@@ -1248,4 +1264,24 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -1248,4 +1264,24 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
1248 onCloseSearchBundle() { 1264 onCloseSearchBundle() {
1249 this.searchBundle = ''; 1265 this.searchBundle = '';
1250 } 1266 }
  1267 +
  1268 + public updateDashboardImage($event: Event) {
  1269 + if ($event) {
  1270 + $event.stopPropagation();
  1271 + }
  1272 + this.dialog.open<DashboardImageDialogComponent, DashboardImageDialogData,
  1273 + DashboardImageDialogResult>(DashboardImageDialogComponent, {
  1274 + disableClose: true,
  1275 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  1276 + data: {
  1277 + dashboardId: this.dashboard.id,
  1278 + currentImage: this.dashboard.image,
  1279 + dashboardElement: this.dashboardContainer.nativeElement
  1280 + }
  1281 + }).afterClosed().subscribe((result) => {
  1282 + if (result) {
  1283 + this.dashboard.image = result.image;
  1284 + }
  1285 + });
  1286 + }
1251 } 1287 }
@@ -69,6 +69,9 @@ @@ -69,6 +69,9 @@
69 <mat-checkbox fxFlex formControlName="showDashboardExport"> 69 <mat-checkbox fxFlex formControlName="showDashboardExport">
70 {{ 'dashboard.display-dashboard-export' | translate }} 70 {{ 'dashboard.display-dashboard-export' | translate }}
71 </mat-checkbox> 71 </mat-checkbox>
  72 + <mat-checkbox fxFlex formControlName="showUpdateDashboardImage">
  73 + {{ 'dashboard.display-update-dashboard-image' | translate }}
  74 + </mat-checkbox>
72 </div> 75 </div>
73 <mat-checkbox formControlName="showDashboardLogo"> 76 <mat-checkbox formControlName="showDashboardLogo">
74 {{ 'dashboard.display-dashboard-logo' | translate }} 77 {{ 'dashboard.display-dashboard-logo' | translate }}
@@ -82,7 +82,8 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS @@ -82,7 +82,8 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
82 showDashboardLogo: [isUndefined(this.settings.showDashboardLogo) ? false : this.settings.showDashboardLogo, []], 82 showDashboardLogo: [isUndefined(this.settings.showDashboardLogo) ? false : this.settings.showDashboardLogo, []],
83 dashboardLogoUrl: [isUndefined(this.settings.dashboardLogoUrl) ? null : this.settings.dashboardLogoUrl, []], 83 dashboardLogoUrl: [isUndefined(this.settings.dashboardLogoUrl) ? null : this.settings.dashboardLogoUrl, []],
84 showDashboardTimewindow: [isUndefined(this.settings.showDashboardTimewindow) ? true : this.settings.showDashboardTimewindow, []], 84 showDashboardTimewindow: [isUndefined(this.settings.showDashboardTimewindow) ? true : this.settings.showDashboardTimewindow, []],
85 - showDashboardExport: [isUndefined(this.settings.showDashboardExport) ? true : this.settings.showDashboardExport, []] 85 + showDashboardExport: [isUndefined(this.settings.showDashboardExport) ? true : this.settings.showDashboardExport, []],
  86 + showUpdateDashboardImage: [isUndefined(this.settings.showUpdateDashboardImage) ? true : this.settings.showUpdateDashboardImage, []]
86 }); 87 });
87 this.settingsFormGroup.get('stateControllerId').valueChanges.subscribe( 88 this.settingsFormGroup.get('stateControllerId').valueChanges.subscribe(
88 (stateControllerId: StateControllerId) => { 89 (stateControllerId: StateControllerId) => {
@@ -141,6 +141,7 @@ import { EdgeDownlinkTableHeaderComponent } from '@home/components/edge/edge-dow @@ -141,6 +141,7 @@ import { EdgeDownlinkTableHeaderComponent } from '@home/components/edge/edge-dow
141 import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component'; 141 import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component';
142 import { SecurityConfigLwm2mComponent } from '@home/components/device/security-config-lwm2m.component'; 142 import { SecurityConfigLwm2mComponent } from '@home/components/device/security-config-lwm2m.component';
143 import { SecurityConfigLwm2mServerComponent } from '@home/components/device/security-config-lwm2m-server.component'; 143 import { SecurityConfigLwm2mServerComponent } from '@home/components/device/security-config-lwm2m-server.component';
  144 +import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component';
144 145
145 @NgModule({ 146 @NgModule({
146 declarations: 147 declarations:
@@ -260,6 +261,7 @@ import { SecurityConfigLwm2mServerComponent } from '@home/components/device/secu @@ -260,6 +261,7 @@ import { SecurityConfigLwm2mServerComponent } from '@home/components/device/secu
260 DashboardSettingsDialogComponent, 261 DashboardSettingsDialogComponent,
261 ManageDashboardStatesDialogComponent, 262 ManageDashboardStatesDialogComponent,
262 DashboardStateDialogComponent, 263 DashboardStateDialogComponent,
  264 + DashboardImageDialogComponent,
263 EmbedDashboardDialogComponent, 265 EmbedDashboardDialogComponent,
264 DisplayWidgetTypesPanelComponent 266 DisplayWidgetTypesPanelComponent
265 ], 267 ],
@@ -370,6 +372,7 @@ import { SecurityConfigLwm2mServerComponent } from '@home/components/device/secu @@ -370,6 +372,7 @@ import { SecurityConfigLwm2mServerComponent } from '@home/components/device/secu
370 DashboardSettingsDialogComponent, 372 DashboardSettingsDialogComponent,
371 ManageDashboardStatesDialogComponent, 373 ManageDashboardStatesDialogComponent,
372 DashboardStateDialogComponent, 374 DashboardStateDialogComponent,
  375 + DashboardImageDialogComponent,
373 EmbedDashboardDialogComponent, 376 EmbedDashboardDialogComponent,
374 DisplayWidgetTypesPanelComponent 377 DisplayWidgetTypesPanelComponent
375 ], 378 ],
@@ -91,6 +91,7 @@ export interface DashboardSettings { @@ -91,6 +91,7 @@ export interface DashboardSettings {
91 dashboardLogoUrl?: string; 91 dashboardLogoUrl?: string;
92 showDashboardTimewindow?: boolean; 92 showDashboardTimewindow?: boolean;
93 showDashboardExport?: boolean; 93 showDashboardExport?: boolean;
  94 + showUpdateDashboardImage?: boolean;
94 toolbarAlwaysOpen?: boolean; 95 toolbarAlwaysOpen?: boolean;
95 titleColor?: string; 96 titleColor?: string;
96 } 97 }
@@ -667,6 +667,8 @@ @@ -667,6 +667,8 @@
667 "add-widget": "Add new widget", 667 "add-widget": "Add new widget",
668 "title": "Title", 668 "title": "Title",
669 "image": "Dashboard image", 669 "image": "Dashboard image",
  670 + "update-image": "Update dashboard image",
  671 + "take-screenshot": "Take screenshot",
670 "select-widget-title": "Select widget", 672 "select-widget-title": "Select widget",
671 "select-widget-value": "{{title}}: select widget", 673 "select-widget-value": "{{title}}: select widget",
672 "select-widget-subtitle": "List of available widget types", 674 "select-widget-subtitle": "List of available widget types",
@@ -748,6 +750,7 @@ @@ -748,6 +750,7 @@
748 "display-filters": "Display filters", 750 "display-filters": "Display filters",
749 "display-dashboard-timewindow": "Display timewindow", 751 "display-dashboard-timewindow": "Display timewindow",
750 "display-dashboard-export": "Display export", 752 "display-dashboard-export": "Display export",
  753 + "display-update-dashboard-image": "Display update dashboard image",
751 "display-dashboard-logo": "Display logo in dashboard fullscreen mode", 754 "display-dashboard-logo": "Display logo in dashboard fullscreen mode",
752 "dashboard-logo-image": "Dashboard logo image", 755 "dashboard-logo-image": "Dashboard logo image",
753 "import": "Import dashboard", 756 "import": "Import dashboard",
@@ -2222,6 +2222,11 @@ base64-arraybuffer@0.1.5: @@ -2222,6 +2222,11 @@ base64-arraybuffer@0.1.5:
2222 resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" 2222 resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
2223 integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= 2223 integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
2224 2224
  2225 +base64-arraybuffer@^0.2.0:
  2226 + version "0.2.0"
  2227 + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
  2228 + integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==
  2229 +
2225 base64-js@^1.0.2: 2230 base64-js@^1.0.2:
2226 version "1.3.1" 2231 version "1.3.1"
2227 resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" 2232 resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
@@ -3206,6 +3211,13 @@ css-declaration-sorter@^4.0.1: @@ -3206,6 +3211,13 @@ css-declaration-sorter@^4.0.1:
3206 postcss "^7.0.1" 3211 postcss "^7.0.1"
3207 timsort "^0.3.0" 3212 timsort "^0.3.0"
3208 3213
  3214 +css-line-break@1.1.1:
  3215 + version "1.1.1"
  3216 + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-1.1.1.tgz#d5e9bdd297840099eb0503c7310fd34927a026ef"
  3217 + integrity sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==
  3218 + dependencies:
  3219 + base64-arraybuffer "^0.2.0"
  3220 +
3209 css-loader@4.2.2: 3221 css-loader@4.2.2:
3210 version "4.2.2" 3222 version "4.2.2"
3211 resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.2.2.tgz#b668b3488d566dc22ebcf9425c5f254a05808c89" 3223 resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.2.2.tgz#b668b3488d566dc22ebcf9425c5f254a05808c89"
@@ -4782,6 +4794,13 @@ html-escaper@^2.0.0: @@ -4782,6 +4794,13 @@ html-escaper@^2.0.0:
4782 resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" 4794 resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
4783 integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== 4795 integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
4784 4796
  4797 +html2canvas@^1.0.0-rc.7:
  4798 + version "1.0.0-rc.7"
  4799 + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.0.0-rc.7.tgz#70c159ce0e63954a91169531894d08ad5627ac98"
  4800 + integrity sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==
  4801 + dependencies:
  4802 + css-line-break "1.1.1"
  4803 +
4785 http-cache-semantics@^3.8.1: 4804 http-cache-semantics@^3.8.1:
4786 version "3.8.1" 4805 version "3.8.1"
4787 resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" 4806 resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"