Add ability to set widget and dashboard level CSS. Introduce dashboard state component with ability to be embedded inside markdown widget.

This commit is contained in:
Igor Kulikov 2021-12-22 17:15:10 +02:00
parent 6b9bdba92f
commit 3874f8ff46
27 changed files with 618 additions and 49 deletions

View File

@ -17,7 +17,7 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, ElementRef,
Component, ElementRef, HostBinding,
Inject,
Injector,
Input,
@ -48,7 +48,7 @@ import {
} from '@app/shared/models/dashboard.models';
import { WINDOW } from '@core/services/window.service';
import { WindowMessage } from '@shared/models/window-message.model';
import { deepClone, isDefined, isDefinedAndNotNull } from '@app/core/utils';
import { deepClone, guid, hashCode, isDefined, isDefinedAndNotNull, isNotEmptyStr } from '@app/core/utils';
import {
DashboardContext,
DashboardPageLayout,
@ -132,6 +132,8 @@ import {
DashboardImageDialogData, DashboardImageDialogResult
} from '@home/components/dashboard-page/dashboard-image-dialog.component';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import cssjs from '@core/css/css';
import { DOCUMENT } from '@angular/common';
// @dynamic
@Component({
@ -147,6 +149,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
authUser: AuthUser = this.authState.authUser;
@HostBinding('class')
dashboardPageClass: string;
@Input()
embedded = false;
@ -302,6 +307,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
constructor(protected store: Store<AppState>,
@Inject(WINDOW) private window: Window,
@Inject(DOCUMENT) private document: Document,
private breakpointObserver: BreakpointObserver,
private route: ActivatedRoute,
private router: Router,
@ -419,6 +425,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.dashboardConfiguration.entityAliases,
this.dashboardConfiguration.filters);
this.updateDashboardCss();
if (this.widgetEditMode) {
const message: WindowMessage = {
type: 'widgetEditModeInited'
@ -427,6 +435,27 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
}
private updateDashboardCss() {
this.cleanupDashboardCss();
const cssString = this.dashboardConfiguration.settings.dashboardCss;
if (isNotEmptyStr(cssString)) {
const cssParser = new cssjs();
cssParser.testMode = false;
this.dashboardPageClass = 'tb-dashboard-page-css-' + guid();
cssParser.cssPreviewNamespace = this.dashboardPageClass;
cssParser.createStyleElement(this.dashboardPageClass, cssString);
}
}
private cleanupDashboardCss() {
if (this.dashboardPageClass) {
const el = this.document.getElementById(this.dashboardPageClass);
if (el) {
el.parentNode.removeChild(el);
}
}
}
private reset() {
this.dashboard = null;
this.translatedDashboardTitle = null;
@ -466,6 +495,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
ngOnDestroy(): void {
this.cleanupDashboardCss();
if (this.isMobileApp && this.syncStateWithQueryParam) {
this.mobileService.unregisterToggleLayoutFunction();
}
@ -729,6 +759,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if (data) {
this.dashboard.configuration.settings = data.settings;
this.dashboardLogoCache = undefined;
this.updateDashboardCss();
const newGridSettings = data.gridSettings;
if (newGridSettings) {
const layout = this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout];
@ -893,6 +924,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.dashboardLogoCache = undefined;
this.dashboardConfiguration = this.dashboard.configuration;
this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
this.updateDashboardCss();
this.entityAliasesUpdated();
this.filtersUpdated();
this.updateLayouts();

View File

@ -87,6 +87,19 @@
{{ 'dashboard.display-update-dashboard-image' | translate }}
</mat-slide-toggle>
</fieldset>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-description fxLayoutAlign="end" translate>
dashboard.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-css
label="{{ 'dashboard.dashboard-css' | translate }}"
formControlName="dashboardCss"
></tb-css>
</ng-template>
</mat-expansion-panel>
</div>
<div *ngIf="gridSettings" [formGroup]="gridSettingsFormGroup" fxLayout="column">
<fieldset class="fields-group" fxLayout="column" fxLayoutGap="8px">

View File

@ -30,4 +30,33 @@
.mat-slide-toggle-content {
white-space: normal;
}
.mat-expansion-panel {
&.tb-settings {
box-shadow: none;
.mat-content {
overflow: visible;
}
.mat-expansion-panel-header {
padding: 0;
&:hover {
background: none;
}
.mat-expansion-indicator {
padding: 2px;
}
}
.mat-expansion-panel-header-description {
align-items: center;
}
.mat-expansion-panel-body{
padding: 0;
}
.tb-css-content-panel {
margin: 0 0 8px;
}
}
.mat-expansion-panel-content {
font: inherit;
}
}
}

View File

@ -97,7 +97,8 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
disabled: hideToolbar}, []],
showUpdateDashboardImage: [
{value: isUndefined(this.settings.showUpdateDashboardImage) ? true : this.settings.showUpdateDashboardImage,
disabled: hideToolbar}, []]
disabled: hideToolbar}, []],
dashboardCss: [isUndefined(this.settings.dashboardCss) ? '' : this.settings.dashboardCss, []],
});
this.settingsFormGroup.get('stateControllerId').valueChanges.subscribe(
(stateControllerId: StateControllerId) => {

View File

@ -0,0 +1,25 @@
<!--
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.
-->
<tb-dashboard-page
[embedded]="true"
[syncStateWithQueryParam]="false"
[hideToolbar]="true"
[currentState]="currentState"
[dashboard]="dashboard"
[parentDashboard]="parentDashboard">
</tb-dashboard-page>

View File

@ -0,0 +1,70 @@
///
/// 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 { Component, Input, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Dashboard } from '@shared/models/dashboard.models';
import { StateObject, StateParams } from '@core/api/widget-api.models';
import { updateEntityParams, WidgetContext } from '../../models/widget-component.models';
import { deepClone, objToBase64 } from '@core/utils';
import { IDashboardComponent } from '@home/models/dashboard-component.models';
import { EntityId } from '@shared/models/id/entity-id';
@Component({
selector: 'tb-dashboard-state',
templateUrl: './dashboard-state.component.html',
styleUrls: []
})
export class DashboardStateComponent extends PageComponent implements OnInit {
@Input()
ctx: WidgetContext;
@Input()
stateId: string;
@Input()
entityParamName: string;
@Input()
entityId: EntityId;
currentState: string;
dashboard: Dashboard;
parentDashboard: IDashboardComponent;
constructor(protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.dashboard = deepClone(this.ctx.stateController.dashboardCtrl.dashboardCtx.getDashboard());
const stateObject: StateObject = {};
const params = deepClone(this.ctx.stateController.getStateParams());
updateEntityParams(params, this.entityParamName, this.entityId);
stateObject.params = params;
if (this.stateId) {
stateObject.id = this.stateId;
}
this.currentState = objToBase64([stateObject]);
this.parentDashboard = this.ctx.parentDashboard ?
this.ctx.parentDashboard : this.ctx.dashboard;
}
}

View File

@ -63,8 +63,9 @@
</mat-menu>
<div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;">
<gridster #gridster id="gridster-child" [options]="gridsterOpts">
<gridster-item [item]="widget" [ngClass]="{'tb-noselect': isEdit}" *ngFor="let widget of dashboardWidgets">
<gridster-item #gridsterItem [item]="widget" [ngClass]="{'tb-noselect': isEdit}" *ngFor="let widget of dashboardWidgets">
<tb-widget-container
[gridsterItem]="gridsterItem"
[widget]="widget"
[dashboardWidgets]="dashboardWidgets"
[dashboardStyle]="dashboardStyle"

View File

@ -143,8 +143,10 @@ import { DeviceCredentialsModule } from '@home/components/device/device-credenti
import { DeviceProfileCommonModule } from '@home/components/profile/device/common/device-profile-common.module';
import {
COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN,
DASHBOARD_PAGE_COMPONENT_TOKEN
DASHBOARD_PAGE_COMPONENT_TOKEN,
HOME_COMPONENTS_MODULE_TOKEN
} from '@home/components/tokens';
import { DashboardStateComponent } from '@home/components/dashboard-page/dashboard-state.component';
@NgModule({
declarations:
@ -252,6 +254,7 @@ import {
TwilioSmsProviderConfigurationComponent,
DashboardToolbarComponent,
DashboardPageComponent,
DashboardStateComponent,
DashboardLayoutComponent,
EditWidgetComponent,
DashboardWidgetSelectComponent,
@ -363,6 +366,7 @@ import {
TwilioSmsProviderConfigurationComponent,
DashboardToolbarComponent,
DashboardPageComponent,
DashboardStateComponent,
DashboardLayoutComponent,
EditWidgetComponent,
DashboardWidgetSelectComponent,
@ -381,7 +385,8 @@ import {
ImportExportService,
{provide: EMBED_DASHBOARD_DIALOG_TOKEN, useValue: EmbedDashboardDialogComponent},
{provide: COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN, useValue: ComplexFilterPredicateDialogComponent},
{provide: DASHBOARD_PAGE_COMPONENT_TOKEN, useValue: DashboardPageComponent}
{provide: DASHBOARD_PAGE_COMPONENT_TOKEN, useValue: DashboardPageComponent},
{provide: HOME_COMPONENTS_MODULE_TOKEN, useValue: HomeComponentsModule }
]
})
export class HomeComponentsModule { }

View File

@ -20,6 +20,9 @@ import { ComponentType } from '@angular/cdk/portal';
export const SHARED_HOME_COMPONENTS_MODULE_TOKEN: InjectionToken<Type<any>> =
new InjectionToken<Type<any>>('SHARED_HOME_COMPONENTS_MODULE_TOKEN');
export const HOME_COMPONENTS_MODULE_TOKEN: InjectionToken<Type<any>> =
new InjectionToken<Type<any>>('HOME_COMPONENTS_MODULE_TOKEN');
export const COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN: InjectionToken<ComponentType<any>> =
new InjectionToken<ComponentType<any>>('COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN');

View File

@ -15,4 +15,5 @@
limitations under the License.
-->
<tb-markdown [data]="markdownText" lineNumbers fallbackToPlainMarkdown (click)="markdownClick($event)"></tb-markdown>
<tb-markdown [data]="markdownText" [additionalCompileModules]="[ homeComponentsModule ]"
[context]="{ ctx: ctx }" lineNumbers fallbackToPlainMarkdown (click)="markdownClick($event)"></tb-markdown>

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, HostBinding, Input, OnInit } from '@angular/core';
import { ChangeDetectorRef, Component, HostBinding, Inject, Input, OnInit, Type } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
@ -32,6 +32,7 @@ import { FormattedData } from '@home/components/widget/lib/maps/map-models';
import { hashCode, isNotEmptyStr } from '@core/utils';
import cssjs from '@core/css/css';
import { UtilsService } from '@core/services/utils.service';
import { HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
interface MarkdownWidgetSettings {
markdownTextPattern: string;
@ -62,6 +63,7 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit {
constructor(protected store: Store<AppState>,
private utils: UtilsService,
@Inject(HOME_COMPONENTS_MODULE_TOKEN) public homeComponentsModule: Type<any>,
private cd: ChangeDetectorRef) {
super(store);
}

View File

@ -45,6 +45,7 @@ import { MODULES_MAP } from '@shared/public-api';
import * as tinycolor_ from 'tinycolor2';
import moment from 'moment';
import { IModulesMap } from '@modules/common/modules-map.models';
import { HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
const tinycolor = tinycolor_;
@ -66,6 +67,7 @@ export class WidgetComponentService {
constructor(@Inject(WINDOW) private window: Window,
@Optional() @Inject(MODULES_MAP) private modulesMap: IModulesMap,
@Inject(HOME_COMPONENTS_MODULE_TOKEN) private homeComponentsModule: Type<any>,
private dynamicComponentFactoryService: DynamicComponentFactoryService,
private widgetService: WidgetService,
private utils: UtilsService,
@ -177,8 +179,10 @@ export class WidgetComponentService {
forkJoin(widgetModulesTasks).subscribe(
() => {
const loadDefaultWidgetInfoTasks = [
this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', [SharedModule, WidgetComponentsModule]),
this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', [SharedModule, WidgetComponentsModule]),
this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type',
[SharedModule, WidgetComponentsModule, this.homeComponentsModule]),
this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type',
[SharedModule, WidgetComponentsModule, this.homeComponentsModule]),
];
forkJoin(loadDefaultWidgetInfoTasks).subscribe(
() => {
@ -274,7 +278,7 @@ export class WidgetComponentService {
}
if (widgetControllerDescriptor) {
const widgetNamespace = `widget-type-${(isSystem ? 'sys-' : '')}${bundleAlias}-${widgetInfo.alias}`;
this.loadWidgetResources(widgetInfo, widgetNamespace, [SharedModule, WidgetComponentsModule]).subscribe(
this.loadWidgetResources(widgetInfo, widgetNamespace, [SharedModule, WidgetComponentsModule, this.homeComponentsModule]).subscribe(
() => {
if (widgetControllerDescriptor.settingsSchema) {
widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema;

View File

@ -418,6 +418,10 @@
label="{{ 'widget-config.widget-style' | translate }}"
formControlName="widgetStyle"
></tb-json-object-edit>
<tb-css
label="{{ 'widget-config.widget-css' | translate }}"
formControlName="widgetCss"
></tb-css>
</ng-template>
</mat-expansion-panel>
</fieldset>

View File

@ -133,7 +133,7 @@
.mat-expansion-panel-body{
padding: 0;
}
.tb-json-object-panel {
.tb-json-object-panel, .tb-css-content-panel {
margin: 0 0 8px;
}
.mat-checkbox-layout {

View File

@ -206,6 +206,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
padding: [null, []],
margin: [null, []],
widgetStyle: [null, []],
widgetCss: [null, []],
titleStyle: [null, []],
units: [null, []],
decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]],
@ -406,6 +407,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
padding: config.padding,
margin: config.margin,
widgetStyle: isDefined(config.widgetStyle) ? config.widgetStyle : {},
widgetCss: isDefined(config.widgetCss) ? config.widgetCss : '',
titleStyle: isDefined(config.titleStyle) ? config.titleStyle : {
fontSize: '16px',
fontWeight: 400

View File

@ -15,7 +15,7 @@
limitations under the License.
-->
<div tb-fullscreen [fullscreen]="widget.isFullscreen"
<div #tbWidgetElement tb-fullscreen [fullscreen]="widget.isFullscreen"
[fullscreenBackgroundStyle]="dashboardStyle"
[fullscreenBackgroundImage]="backgroundImage"
(fullscreenChanged)="onFullscreenChanged($event)"

View File

@ -17,17 +17,21 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
Input,
Component, ElementRef,
EventEmitter, HostBinding, Inject,
Input, OnDestroy,
OnInit,
Output
Output, Renderer2, ViewChild
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { DashboardWidget, DashboardWidgets } from '@home/models/dashboard-component.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { SafeStyle } from '@angular/platform-browser';
import { guid, hashCode, isNotEmptyStr } from '@core/utils';
import cssjs from '@core/css/css';
import { DOCUMENT } from '@angular/common';
import { GridsterItemComponent } from 'angular-gridster2';
export enum WidgetComponentActionType {
MOUSE_DOWN,
@ -43,13 +47,23 @@ export class WidgetComponentAction {
actionType: WidgetComponentActionType;
}
// @dynamic
@Component({
selector: 'tb-widget-container',
templateUrl: './widget-container.component.html',
styleUrls: ['./widget-container.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class WidgetContainerComponent extends PageComponent implements OnInit {
export class WidgetContainerComponent extends PageComponent implements OnInit, OnDestroy {
@HostBinding('class')
widgetContainerClass = 'tb-widget-container';
@ViewChild('tbWidgetElement', {static: true})
tbWidgetElement: ElementRef;
@Input()
gridsterItem: GridsterItemComponent;
@Input()
widget: DashboardWidget;
@ -87,13 +101,35 @@ export class WidgetContainerComponent extends PageComponent implements OnInit {
@Output()
widgetComponentAction: EventEmitter<WidgetComponentAction> = new EventEmitter<WidgetComponentAction>();
private cssClass: string;
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef) {
private cd: ChangeDetectorRef,
private renderer: Renderer2,
@Inject(DOCUMENT) private document: Document) {
super(store);
}
ngOnInit(): void {
this.widget.widgetContext.containerChangeDetector = this.cd;
const cssString = this.widget.widget.config.widgetCss;
if (isNotEmptyStr(cssString)) {
const cssParser = new cssjs();
cssParser.testMode = false;
this.cssClass = 'tb-widget-css-' + guid();
this.renderer.addClass(this.gridsterItem.el, this.cssClass);
cssParser.cssPreviewNamespace = this.cssClass;
cssParser.createStyleElement(this.cssClass, cssString);
}
}
ngOnDestroy(): void {
if (this.cssClass) {
const el = this.document.getElementById(this.cssClass);
if (el) {
el.parentNode.removeChild(el);
}
}
}
isHighlighted(widget: DashboardWidget) {
@ -105,6 +141,11 @@ export class WidgetContainerComponent extends PageComponent implements OnInit {
}
onFullscreenChanged(expanded: boolean) {
if (expanded) {
this.renderer.addClass(this.tbWidgetElement.nativeElement, this.cssClass);
} else {
this.renderer.removeClass(this.tbWidgetElement.nativeElement, this.cssClass);
}
this.widgetFullscreenChanged.emit(expanded);
}

View File

@ -65,7 +65,7 @@ import {
validateEntityId
} from '@core/utils';
import {
IDynamicWidgetComponent, ShowWidgetHeaderActionFunction,
IDynamicWidgetComponent, ShowWidgetHeaderActionFunction, updateEntityParams,
WidgetContext,
WidgetHeaderAction,
WidgetInfo,
@ -1070,7 +1070,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
case WidgetActionType.updateDashboardState:
let targetDashboardStateId = descriptor.targetDashboardStateId;
const params = deepClone(this.widgetContext.stateController.getStateParams());
this.updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel);
updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel);
if (type === WidgetActionType.openDashboardState) {
if (descriptor.openInPopover) {
this.openDashboardStateInPopover($event, descriptor.targetDashboardStateId, params,
@ -1091,7 +1091,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
targetDashboardStateId = descriptor.targetDashboardStateId;
const stateObject: StateObject = {};
stateObject.params = {};
this.updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName, entityLabel);
updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName, entityLabel);
if (targetDashboardStateId) {
stateObject.id = targetDashboardStateId;
}
@ -1360,7 +1360,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetContentContainer, this.dashboardPageComponent, preferredPlacement, hideOnClickOutside,
injector,
{
embed: true,
embedded: true,
syncStateWithQueryParam: false,
hideToolbar: hideDashboardToolbar,
currentState: objToBase64([stateObject]),
@ -1443,30 +1443,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
}
private updateEntityParams(params: StateParams, targetEntityParamName?: string, targetEntityId?: EntityId,
entityName?: string, entityLabel?: string) {
if (targetEntityId) {
let targetEntityParams: StateParams;
if (targetEntityParamName && targetEntityParamName.length) {
targetEntityParams = params[targetEntityParamName];
if (!targetEntityParams) {
targetEntityParams = {};
params[targetEntityParamName] = targetEntityParams;
params.targetEntityParamName = targetEntityParamName;
}
} else {
targetEntityParams = params;
}
targetEntityParams.entityId = targetEntityId;
if (entityName) {
targetEntityParams.entityName = entityName;
}
if (entityLabel) {
targetEntityParams.entityLabel = entityLabel;
}
}
}
private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array<WidgetResource>): Observable<any> {
if (isDefined(customCss) && customCss.length > 0) {
this.cssParser.cssPreviewNamespace = actionNamespace;

View File

@ -37,7 +37,7 @@ import {
IStateController,
IWidgetSubscription,
IWidgetUtils,
RpcApi,
RpcApi, StateParams,
SubscriptionEntityInfo,
TimewindowFunctions,
WidgetActionsApi,
@ -81,6 +81,7 @@ import { Router } from '@angular/router';
import { catchError, map, mergeMap, switchMap } from 'rxjs/operators';
import { FormattedData } from '@home/components/widget/lib/maps/map-models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { EntityId } from '@shared/models/id/entity-id';
export interface IWidgetAction {
name: string;
@ -545,3 +546,27 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
descriptor
};
}
export function updateEntityParams(params: StateParams, targetEntityParamName?: string, targetEntityId?: EntityId,
entityName?: string, entityLabel?: string) {
if (targetEntityId) {
let targetEntityParams: StateParams;
if (targetEntityParamName && targetEntityParamName.length) {
targetEntityParams = params[targetEntityParamName];
if (!targetEntityParams) {
targetEntityParams = {};
params[targetEntityParamName] = targetEntityParams;
params.targetEntityParamName = targetEntityParamName;
}
} else {
targetEntityParams = params;
}
targetEntityParams.entityId = targetEntityId;
if (entityName) {
targetEntityParams.entityName = entityName;
}
if (entityLabel) {
targetEntityParams.entityLabel = entityLabel;
}
}
}

View File

@ -0,0 +1,41 @@
<!--
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.
-->
<div class="tb-css" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight}"
tb-fullscreen
[fullscreen]="fullscreen" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-css-toolbar">
<label class="tb-title no-padding" [ngClass]="{'tb-error': hasErrors}">{{ label }}</label>
<span fxFlex></span>
<button type='button' *ngIf="!disabled" mat-button class="tidy" (click)="beautifyCss()">
{{'js-func.tidy' | translate }}
</button>
<fieldset style="width: initial">
<div matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above"
style="border-radius: 50%"
(click)="fullscreen = !fullscreen">
<button type='button' mat-button mat-icon-button class="tb-mat-32">
<mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
</fieldset>
</div>
<div id="tb-css-panel" class="tb-css-content-panel" fxLayout="column">
<div #cssEditor id="tb-css-input" [ngClass]="{'fill-height': fillHeight}"></div>
</div>
</div>

View File

@ -0,0 +1,66 @@
/**
* 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.
*/
.tb-css {
position: relative;
&.tb-disabled {
color: rgba(0, 0, 0, .38);
}
&.fill-height {
height: 100%;
}
.tb-css-content-panel {
height: calc(100% - 80px);
margin-left: 15px;
border: 1px solid #c0c0c0;
#tb-css-input {
width: 100%;
min-width: 200px;
height: 100%;
&:not(.fill-height) {
min-height: 200px;
}
}
}
.tb-css-toolbar {
& > * {
&:not(:last-child) {
margin-right: 4px;
}
}
button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
background: rgba(220, 220, 220, .35);
align-items: center;
vertical-align: middle;
min-width: 32px;
min-height: 15px;
padding: 4px;
font-size: .8rem;
line-height: 15px;
&:not(.tb-help-popup-button) {
color: #7b7b7b;
}
}
.tb-help-popup-button-loading {
background: #f3f3f3;
}
}
}

View File

@ -0,0 +1,207 @@
///
/// 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 {
Component,
ElementRef,
forwardRef,
Input,
OnDestroy,
OnInit,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
import { Ace } from 'ace-builds';
import { getAce } from '@shared/models/ace/ace.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { ResizeObserver } from '@juggle/resize-observer';
import { beautifyCss } from '@shared/models/beautify.models';
@Component({
selector: 'tb-css',
templateUrl: './css.component.html',
styleUrls: ['./css.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CssComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CssComponent),
multi: true,
}
],
encapsulation: ViewEncapsulation.None
})
export class CssComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
@ViewChild('cssEditor', {static: true})
cssEditorElmRef: ElementRef;
private cssEditor: Ace.Editor;
private editorsResizeCaf: CancelAnimationFrame;
private editorResize$: ResizeObserver;
private ignoreChange = false;
@Input() label: string;
@Input() disabled: boolean;
@Input() fillHeight: boolean;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
fullscreen = false;
modelValue: string;
hasErrors = false;
private propagateChange = null;
constructor(public elementRef: ElementRef,
private utils: UtilsService,
private translate: TranslateService,
protected store: Store<AppState>,
private raf: RafService) {
}
ngOnInit(): void {
const editorElement = this.cssEditorElmRef.nativeElement;
let editorOptions: Partial<Ace.EditorOptions> = {
mode: 'ace/mode/css',
showGutter: true,
showPrintMargin: true,
readOnly: this.disabled
};
const advancedOptions = {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
};
editorOptions = {...editorOptions, ...advancedOptions};
getAce().subscribe(
(ace) => {
this.cssEditor = ace.edit(editorElement, editorOptions);
this.cssEditor.session.setUseWrapMode(true);
this.cssEditor.setValue(this.modelValue ? this.modelValue : '', -1);
this.cssEditor.setReadOnly(this.disabled);
this.cssEditor.on('change', () => {
if (!this.ignoreChange) {
this.updateView();
}
});
// @ts-ignore
this.cssEditor.session.on('changeAnnotation', () => {
const annotations = this.cssEditor.session.getAnnotations();
const hasErrors = annotations.filter(annotation => annotation.type === 'error').length > 0;
if (this.hasErrors !== hasErrors) {
this.hasErrors = hasErrors;
this.propagateChange(this.modelValue);
}
});
this.editorResize$ = new ResizeObserver(() => {
this.onAceEditorResize();
});
this.editorResize$.observe(editorElement);
}
);
}
ngOnDestroy(): void {
if (this.editorResize$) {
this.editorResize$.disconnect();
}
}
private onAceEditorResize() {
if (this.editorsResizeCaf) {
this.editorsResizeCaf();
this.editorsResizeCaf = null;
}
this.editorsResizeCaf = this.raf.raf(() => {
this.cssEditor.resize();
this.cssEditor.renderer.updateFull();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.cssEditor) {
this.cssEditor.setReadOnly(this.disabled);
}
}
public validate(c: FormControl) {
return (!this.hasErrors) ? null : {
css: {
valid: false,
},
};
}
beautifyCss() {
beautifyCss(this.modelValue, {indent_size: 4}).subscribe(
(res) => {
if (this.modelValue !== res) {
this.cssEditor.setValue(res ? res : '', -1);
this.updateView();
}
}
);
}
writeValue(value: string): void {
this.modelValue = value;
if (this.cssEditor) {
this.ignoreChange = true;
this.cssEditor.setValue(this.modelValue ? this.modelValue : '', -1);
this.ignoreChange = false;
}
}
updateView() {
const editorValue = this.cssEditor.getValue();
if (this.modelValue !== editorValue) {
this.modelValue = editorValue;
this.propagateChange(this.modelValue);
}
}
}

View File

@ -47,6 +47,10 @@ export class TbMarkdownComponent implements OnChanges {
@Input() data: string | undefined;
@Input() context: any;
@Input() additionalCompileModules: Type<any>[];
@Input() markdownClass: string | undefined;
@Input() style: { [klass: string]: any } = {};
@ -94,6 +98,10 @@ export class TbMarkdownComponent implements OnChanges {
this.markdownContainer.clear();
const parent = this;
let readyObservable: Observable<void>;
let compileModules = [this.sharedModule];
if (this.additionalCompileModules) {
compileModules = compileModules.concat(this.additionalCompileModules);
}
this.dynamicComponentFactoryService.createDynamicComponentFactory(
class TbMarkdownInstance {
ngOnDestroy(): void {
@ -101,7 +109,7 @@ export class TbMarkdownComponent implements OnChanges {
}
},
template,
[this.sharedModule],
compileModules,
true
).subscribe((factory) => {
this.tbMarkdownInstanceComponentFactory = factory;
@ -109,6 +117,11 @@ export class TbMarkdownComponent implements OnChanges {
try {
this.tbMarkdownInstanceComponentRef =
this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector);
if (this.context) {
for (const propName of Object.keys(this.context)) {
this.tbMarkdownInstanceComponentRef.instance[propName] = this.context[propName];
}
}
this.tbMarkdownInstanceComponentRef.instance.style = this.style;
this.handlePlugins(this.tbMarkdownInstanceComponentRef.location.nativeElement);
this.markdownService.highlight(this.tbMarkdownInstanceComponentRef.location.nativeElement);

View File

@ -97,6 +97,7 @@ export interface DashboardSettings {
toolbarAlwaysOpen?: boolean;
hideToolbar?: boolean;
titleColor?: string;
dashboardCss?: string;
}
export interface DashboardConfiguration {

View File

@ -506,6 +506,7 @@ export interface WidgetConfig {
padding?: string;
margin?: string;
widgetStyle?: {[klass: string]: any};
widgetCss?: string;
titleStyle?: {[klass: string]: any};
units?: string;
decimals?: number;

View File

@ -156,6 +156,7 @@ import { TbPopoverService } from '@shared/components/popover.service';
import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens';
import { TbMarkdownComponent } from '@shared/components/markdown.component';
import { ProtobufContentComponent } from '@shared/components/protobuf-content.component';
import { CssComponent } from '@shared/components/css.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -233,6 +234,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
JsonObjectEditComponent,
JsonContentComponent,
JsFuncComponent,
CssComponent,
FabTriggerDirective,
FabActionsDirective,
FabToolbarComponent,
@ -378,6 +380,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
JsonObjectEditComponent,
JsonContentComponent,
JsFuncComponent,
CssComponent,
FabTriggerDirective,
FabActionsDirective,
FabToolbarComponent,

View File

@ -811,6 +811,8 @@
"dashboard-logo-settings": "Dashboard logo settings",
"display-dashboard-logo": "Display logo in dashboard fullscreen mode",
"dashboard-logo-image": "Dashboard logo image",
"advanced-settings": "Advanced settings",
"dashboard-css": "Dashboard CSS",
"import": "Import dashboard",
"export": "Export dashboard",
"export-failed-error": "Unable to export dashboard: {{error}}",
@ -3084,6 +3086,7 @@
"padding": "Padding",
"margin": "Margin",
"widget-style": "Widget style",
"widget-css": "Widget CSS",
"title-style": "Title style",
"mobile-mode-settings": "Mobile mode",
"order": "Order",