diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 37ef5e0429..e15f04d766 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -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, @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(); diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html index 14c2df301b..0c0a430fdb 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html @@ -87,6 +87,19 @@ {{ 'dashboard.display-update-dashboard-image' | translate }} + + + + dashboard.advanced-settings + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.scss index 5587d9c35a..47845eb9b0 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.scss @@ -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; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts index 5d17d33eaa..c2d01f3d9c 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts @@ -97,7 +97,8 @@ export class DashboardSettingsDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.html new file mode 100644 index 0000000000..24c3eadb08 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.html @@ -0,0 +1,25 @@ + + + diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.ts new file mode 100644 index 0000000000..00bd266c2a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.ts @@ -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) { + 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; + } +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html index c48030050d..dafb67df0b 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html @@ -63,8 +63,9 @@
- + > = new InjectionToken>('SHARED_HOME_COMPONENTS_MODULE_TOKEN'); +export const HOME_COMPONENTS_MODULE_TOKEN: InjectionToken> = + new InjectionToken>('HOME_COMPONENTS_MODULE_TOKEN'); + export const COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN: InjectionToken> = new InjectionToken>('COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN'); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html index 46069c15f0..b808897845 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html @@ -15,4 +15,5 @@ limitations under the License. --> - + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts index fd0197996e..60dea214bc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts @@ -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, private utils: UtilsService, + @Inject(HOME_COMPONENTS_MODULE_TOKEN) public homeComponentsModule: Type, private cd: ChangeDetectorRef) { super(store); } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index 90b3087ced..7dcac82833 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -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, 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; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 97542fd3cb..1073b32e42 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -418,6 +418,10 @@ label="{{ 'widget-config.widget-style' | translate }}" formControlName="widgetStyle" > +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss index c29afa5629..e6fd1a509e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss @@ -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 { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 7d779a3a6c..29af888e37 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -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 diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html index 09f3199b06..3a2712f4f6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
= new EventEmitter(); + private cssClass: string; + constructor(protected store: Store, - 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); } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index c1208d0241..e85d507e9b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -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): Observable { if (isDefined(customCss) && customCss.length > 0) { this.cssParser.cssPreviewNamespace = actionNamespace; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 2a530c8b4e..402e414f02 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -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; + } + } +} diff --git a/ui-ngx/src/app/shared/components/css.component.html b/ui-ngx/src/app/shared/components/css.component.html new file mode 100644 index 0000000000..33d19672c3 --- /dev/null +++ b/ui-ngx/src/app/shared/components/css.component.html @@ -0,0 +1,41 @@ + +
+
+ + + +
+
+ +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/shared/components/css.component.scss b/ui-ngx/src/app/shared/components/css.component.scss new file mode 100644 index 0000000000..dd045bbd2d --- /dev/null +++ b/ui-ngx/src/app/shared/components/css.component.scss @@ -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; + } + } +} diff --git a/ui-ngx/src/app/shared/components/css.component.ts b/ui-ngx/src/app/shared/components/css.component.ts new file mode 100644 index 0000000000..c1367d04ee --- /dev/null +++ b/ui-ngx/src/app/shared/components/css.component.ts @@ -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, + private raf: RafService) { + } + + ngOnInit(): void { + const editorElement = this.cssEditorElmRef.nativeElement; + let editorOptions: Partial = { + 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); + } + } +} diff --git a/ui-ngx/src/app/shared/components/markdown.component.ts b/ui-ngx/src/app/shared/components/markdown.component.ts index 1abfc5b9e6..29933cba21 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.ts +++ b/ui-ngx/src/app/shared/components/markdown.component.ts @@ -47,6 +47,10 @@ export class TbMarkdownComponent implements OnChanges { @Input() data: string | undefined; + @Input() context: any; + + @Input() additionalCompileModules: Type[]; + @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; + 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); diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index db20056fde..f00e0e3878 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -97,6 +97,7 @@ export interface DashboardSettings { toolbarAlwaysOpen?: boolean; hideToolbar?: boolean; titleColor?: string; + dashboardCss?: string; } export interface DashboardConfiguration { diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 0b0353c7a9..8e8aed3e53 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -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; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 1f66aedf47..086768c854 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -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, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 170cea807b..807f3781a9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -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",