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:
parent
6b9bdba92f
commit
3874f8ff46
@ -17,7 +17,7 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component, ElementRef,
|
Component, ElementRef, HostBinding,
|
||||||
Inject,
|
Inject,
|
||||||
Injector,
|
Injector,
|
||||||
Input,
|
Input,
|
||||||
@ -48,7 +48,7 @@ import {
|
|||||||
} from '@app/shared/models/dashboard.models';
|
} from '@app/shared/models/dashboard.models';
|
||||||
import { WINDOW } from '@core/services/window.service';
|
import { WINDOW } from '@core/services/window.service';
|
||||||
import { WindowMessage } from '@shared/models/window-message.model';
|
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 {
|
import {
|
||||||
DashboardContext,
|
DashboardContext,
|
||||||
DashboardPageLayout,
|
DashboardPageLayout,
|
||||||
@ -132,6 +132,8 @@ import {
|
|||||||
DashboardImageDialogData, DashboardImageDialogResult
|
DashboardImageDialogData, DashboardImageDialogResult
|
||||||
} from '@home/components/dashboard-page/dashboard-image-dialog.component';
|
} from '@home/components/dashboard-page/dashboard-image-dialog.component';
|
||||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||||
|
import cssjs from '@core/css/css';
|
||||||
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
|
||||||
// @dynamic
|
// @dynamic
|
||||||
@Component({
|
@Component({
|
||||||
@ -147,6 +149,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
|
|||||||
|
|
||||||
authUser: AuthUser = this.authState.authUser;
|
authUser: AuthUser = this.authState.authUser;
|
||||||
|
|
||||||
|
@HostBinding('class')
|
||||||
|
dashboardPageClass: string;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
embedded = false;
|
embedded = false;
|
||||||
|
|
||||||
@ -302,6 +307,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
|
|||||||
|
|
||||||
constructor(protected store: Store<AppState>,
|
constructor(protected store: Store<AppState>,
|
||||||
@Inject(WINDOW) private window: Window,
|
@Inject(WINDOW) private window: Window,
|
||||||
|
@Inject(DOCUMENT) private document: Document,
|
||||||
private breakpointObserver: BreakpointObserver,
|
private breakpointObserver: BreakpointObserver,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@ -419,6 +425,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
|
|||||||
this.dashboardConfiguration.entityAliases,
|
this.dashboardConfiguration.entityAliases,
|
||||||
this.dashboardConfiguration.filters);
|
this.dashboardConfiguration.filters);
|
||||||
|
|
||||||
|
this.updateDashboardCss();
|
||||||
|
|
||||||
if (this.widgetEditMode) {
|
if (this.widgetEditMode) {
|
||||||
const message: WindowMessage = {
|
const message: WindowMessage = {
|
||||||
type: 'widgetEditModeInited'
|
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() {
|
private reset() {
|
||||||
this.dashboard = null;
|
this.dashboard = null;
|
||||||
this.translatedDashboardTitle = null;
|
this.translatedDashboardTitle = null;
|
||||||
@ -466,6 +495,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
this.cleanupDashboardCss();
|
||||||
if (this.isMobileApp && this.syncStateWithQueryParam) {
|
if (this.isMobileApp && this.syncStateWithQueryParam) {
|
||||||
this.mobileService.unregisterToggleLayoutFunction();
|
this.mobileService.unregisterToggleLayoutFunction();
|
||||||
}
|
}
|
||||||
@ -729,6 +759,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
|
|||||||
if (data) {
|
if (data) {
|
||||||
this.dashboard.configuration.settings = data.settings;
|
this.dashboard.configuration.settings = data.settings;
|
||||||
this.dashboardLogoCache = undefined;
|
this.dashboardLogoCache = undefined;
|
||||||
|
this.updateDashboardCss();
|
||||||
const newGridSettings = data.gridSettings;
|
const newGridSettings = data.gridSettings;
|
||||||
if (newGridSettings) {
|
if (newGridSettings) {
|
||||||
const layout = this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout];
|
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.dashboardLogoCache = undefined;
|
||||||
this.dashboardConfiguration = this.dashboard.configuration;
|
this.dashboardConfiguration = this.dashboard.configuration;
|
||||||
this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
|
this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
|
||||||
|
this.updateDashboardCss();
|
||||||
this.entityAliasesUpdated();
|
this.entityAliasesUpdated();
|
||||||
this.filtersUpdated();
|
this.filtersUpdated();
|
||||||
this.updateLayouts();
|
this.updateLayouts();
|
||||||
|
|||||||
@ -87,6 +87,19 @@
|
|||||||
{{ 'dashboard.display-update-dashboard-image' | translate }}
|
{{ 'dashboard.display-update-dashboard-image' | translate }}
|
||||||
</mat-slide-toggle>
|
</mat-slide-toggle>
|
||||||
</fieldset>
|
</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>
|
||||||
<div *ngIf="gridSettings" [formGroup]="gridSettingsFormGroup" fxLayout="column">
|
<div *ngIf="gridSettings" [formGroup]="gridSettingsFormGroup" fxLayout="column">
|
||||||
<fieldset class="fields-group" fxLayout="column" fxLayoutGap="8px">
|
<fieldset class="fields-group" fxLayout="column" fxLayoutGap="8px">
|
||||||
|
|||||||
@ -30,4 +30,33 @@
|
|||||||
.mat-slide-toggle-content {
|
.mat-slide-toggle-content {
|
||||||
white-space: normal;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,7 +97,8 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
|
|||||||
disabled: hideToolbar}, []],
|
disabled: hideToolbar}, []],
|
||||||
showUpdateDashboardImage: [
|
showUpdateDashboardImage: [
|
||||||
{value: isUndefined(this.settings.showUpdateDashboardImage) ? true : this.settings.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(
|
this.settingsFormGroup.get('stateControllerId').valueChanges.subscribe(
|
||||||
(stateControllerId: StateControllerId) => {
|
(stateControllerId: StateControllerId) => {
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -63,8 +63,9 @@
|
|||||||
</mat-menu>
|
</mat-menu>
|
||||||
<div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;">
|
<div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;">
|
||||||
<gridster #gridster id="gridster-child" [options]="gridsterOpts">
|
<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
|
<tb-widget-container
|
||||||
|
[gridsterItem]="gridsterItem"
|
||||||
[widget]="widget"
|
[widget]="widget"
|
||||||
[dashboardWidgets]="dashboardWidgets"
|
[dashboardWidgets]="dashboardWidgets"
|
||||||
[dashboardStyle]="dashboardStyle"
|
[dashboardStyle]="dashboardStyle"
|
||||||
|
|||||||
@ -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 { DeviceProfileCommonModule } from '@home/components/profile/device/common/device-profile-common.module';
|
||||||
import {
|
import {
|
||||||
COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN,
|
COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN,
|
||||||
DASHBOARD_PAGE_COMPONENT_TOKEN
|
DASHBOARD_PAGE_COMPONENT_TOKEN,
|
||||||
|
HOME_COMPONENTS_MODULE_TOKEN
|
||||||
} from '@home/components/tokens';
|
} from '@home/components/tokens';
|
||||||
|
import { DashboardStateComponent } from '@home/components/dashboard-page/dashboard-state.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations:
|
declarations:
|
||||||
@ -252,6 +254,7 @@ import {
|
|||||||
TwilioSmsProviderConfigurationComponent,
|
TwilioSmsProviderConfigurationComponent,
|
||||||
DashboardToolbarComponent,
|
DashboardToolbarComponent,
|
||||||
DashboardPageComponent,
|
DashboardPageComponent,
|
||||||
|
DashboardStateComponent,
|
||||||
DashboardLayoutComponent,
|
DashboardLayoutComponent,
|
||||||
EditWidgetComponent,
|
EditWidgetComponent,
|
||||||
DashboardWidgetSelectComponent,
|
DashboardWidgetSelectComponent,
|
||||||
@ -363,6 +366,7 @@ import {
|
|||||||
TwilioSmsProviderConfigurationComponent,
|
TwilioSmsProviderConfigurationComponent,
|
||||||
DashboardToolbarComponent,
|
DashboardToolbarComponent,
|
||||||
DashboardPageComponent,
|
DashboardPageComponent,
|
||||||
|
DashboardStateComponent,
|
||||||
DashboardLayoutComponent,
|
DashboardLayoutComponent,
|
||||||
EditWidgetComponent,
|
EditWidgetComponent,
|
||||||
DashboardWidgetSelectComponent,
|
DashboardWidgetSelectComponent,
|
||||||
@ -381,7 +385,8 @@ import {
|
|||||||
ImportExportService,
|
ImportExportService,
|
||||||
{provide: EMBED_DASHBOARD_DIALOG_TOKEN, useValue: EmbedDashboardDialogComponent},
|
{provide: EMBED_DASHBOARD_DIALOG_TOKEN, useValue: EmbedDashboardDialogComponent},
|
||||||
{provide: COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN, useValue: ComplexFilterPredicateDialogComponent},
|
{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 { }
|
export class HomeComponentsModule { }
|
||||||
|
|||||||
@ -20,6 +20,9 @@ import { ComponentType } from '@angular/cdk/portal';
|
|||||||
export const SHARED_HOME_COMPONENTS_MODULE_TOKEN: InjectionToken<Type<any>> =
|
export const SHARED_HOME_COMPONENTS_MODULE_TOKEN: InjectionToken<Type<any>> =
|
||||||
new InjectionToken<Type<any>>('SHARED_HOME_COMPONENTS_MODULE_TOKEN');
|
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>> =
|
export const COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN: InjectionToken<ComponentType<any>> =
|
||||||
new InjectionToken<ComponentType<any>>('COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN');
|
new InjectionToken<ComponentType<any>>('COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN');
|
||||||
|
|
||||||
|
|||||||
@ -15,4 +15,5 @@
|
|||||||
limitations under the License.
|
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>
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
/// limitations under the License.
|
/// 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 { PageComponent } from '@shared/components/page.component';
|
||||||
import { WidgetContext } from '@home/models/widget-component.models';
|
import { WidgetContext } from '@home/models/widget-component.models';
|
||||||
import { Store } from '@ngrx/store';
|
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 { hashCode, isNotEmptyStr } from '@core/utils';
|
||||||
import cssjs from '@core/css/css';
|
import cssjs from '@core/css/css';
|
||||||
import { UtilsService } from '@core/services/utils.service';
|
import { UtilsService } from '@core/services/utils.service';
|
||||||
|
import { HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
|
||||||
|
|
||||||
interface MarkdownWidgetSettings {
|
interface MarkdownWidgetSettings {
|
||||||
markdownTextPattern: string;
|
markdownTextPattern: string;
|
||||||
@ -62,6 +63,7 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(protected store: Store<AppState>,
|
constructor(protected store: Store<AppState>,
|
||||||
private utils: UtilsService,
|
private utils: UtilsService,
|
||||||
|
@Inject(HOME_COMPONENTS_MODULE_TOKEN) public homeComponentsModule: Type<any>,
|
||||||
private cd: ChangeDetectorRef) {
|
private cd: ChangeDetectorRef) {
|
||||||
super(store);
|
super(store);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ import { MODULES_MAP } from '@shared/public-api';
|
|||||||
import * as tinycolor_ from 'tinycolor2';
|
import * as tinycolor_ from 'tinycolor2';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { IModulesMap } from '@modules/common/modules-map.models';
|
import { IModulesMap } from '@modules/common/modules-map.models';
|
||||||
|
import { HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
|
||||||
|
|
||||||
const tinycolor = tinycolor_;
|
const tinycolor = tinycolor_;
|
||||||
|
|
||||||
@ -66,6 +67,7 @@ export class WidgetComponentService {
|
|||||||
|
|
||||||
constructor(@Inject(WINDOW) private window: Window,
|
constructor(@Inject(WINDOW) private window: Window,
|
||||||
@Optional() @Inject(MODULES_MAP) private modulesMap: IModulesMap,
|
@Optional() @Inject(MODULES_MAP) private modulesMap: IModulesMap,
|
||||||
|
@Inject(HOME_COMPONENTS_MODULE_TOKEN) private homeComponentsModule: Type<any>,
|
||||||
private dynamicComponentFactoryService: DynamicComponentFactoryService,
|
private dynamicComponentFactoryService: DynamicComponentFactoryService,
|
||||||
private widgetService: WidgetService,
|
private widgetService: WidgetService,
|
||||||
private utils: UtilsService,
|
private utils: UtilsService,
|
||||||
@ -177,8 +179,10 @@ export class WidgetComponentService {
|
|||||||
forkJoin(widgetModulesTasks).subscribe(
|
forkJoin(widgetModulesTasks).subscribe(
|
||||||
() => {
|
() => {
|
||||||
const loadDefaultWidgetInfoTasks = [
|
const loadDefaultWidgetInfoTasks = [
|
||||||
this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', [SharedModule, WidgetComponentsModule]),
|
this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type',
|
||||||
this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', [SharedModule, WidgetComponentsModule]),
|
[SharedModule, WidgetComponentsModule, this.homeComponentsModule]),
|
||||||
|
this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type',
|
||||||
|
[SharedModule, WidgetComponentsModule, this.homeComponentsModule]),
|
||||||
];
|
];
|
||||||
forkJoin(loadDefaultWidgetInfoTasks).subscribe(
|
forkJoin(loadDefaultWidgetInfoTasks).subscribe(
|
||||||
() => {
|
() => {
|
||||||
@ -274,7 +278,7 @@ export class WidgetComponentService {
|
|||||||
}
|
}
|
||||||
if (widgetControllerDescriptor) {
|
if (widgetControllerDescriptor) {
|
||||||
const widgetNamespace = `widget-type-${(isSystem ? 'sys-' : '')}${bundleAlias}-${widgetInfo.alias}`;
|
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) {
|
if (widgetControllerDescriptor.settingsSchema) {
|
||||||
widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema;
|
widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema;
|
||||||
|
|||||||
@ -418,6 +418,10 @@
|
|||||||
label="{{ 'widget-config.widget-style' | translate }}"
|
label="{{ 'widget-config.widget-style' | translate }}"
|
||||||
formControlName="widgetStyle"
|
formControlName="widgetStyle"
|
||||||
></tb-json-object-edit>
|
></tb-json-object-edit>
|
||||||
|
<tb-css
|
||||||
|
label="{{ 'widget-config.widget-css' | translate }}"
|
||||||
|
formControlName="widgetCss"
|
||||||
|
></tb-css>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@ -133,7 +133,7 @@
|
|||||||
.mat-expansion-panel-body{
|
.mat-expansion-panel-body{
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.tb-json-object-panel {
|
.tb-json-object-panel, .tb-css-content-panel {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
}
|
}
|
||||||
.mat-checkbox-layout {
|
.mat-checkbox-layout {
|
||||||
|
|||||||
@ -206,6 +206,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
|
|||||||
padding: [null, []],
|
padding: [null, []],
|
||||||
margin: [null, []],
|
margin: [null, []],
|
||||||
widgetStyle: [null, []],
|
widgetStyle: [null, []],
|
||||||
|
widgetCss: [null, []],
|
||||||
titleStyle: [null, []],
|
titleStyle: [null, []],
|
||||||
units: [null, []],
|
units: [null, []],
|
||||||
decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]],
|
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,
|
padding: config.padding,
|
||||||
margin: config.margin,
|
margin: config.margin,
|
||||||
widgetStyle: isDefined(config.widgetStyle) ? config.widgetStyle : {},
|
widgetStyle: isDefined(config.widgetStyle) ? config.widgetStyle : {},
|
||||||
|
widgetCss: isDefined(config.widgetCss) ? config.widgetCss : '',
|
||||||
titleStyle: isDefined(config.titleStyle) ? config.titleStyle : {
|
titleStyle: isDefined(config.titleStyle) ? config.titleStyle : {
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
fontWeight: 400
|
fontWeight: 400
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
|
|
||||||
-->
|
-->
|
||||||
<div tb-fullscreen [fullscreen]="widget.isFullscreen"
|
<div #tbWidgetElement tb-fullscreen [fullscreen]="widget.isFullscreen"
|
||||||
[fullscreenBackgroundStyle]="dashboardStyle"
|
[fullscreenBackgroundStyle]="dashboardStyle"
|
||||||
[fullscreenBackgroundImage]="backgroundImage"
|
[fullscreenBackgroundImage]="backgroundImage"
|
||||||
(fullscreenChanged)="onFullscreenChanged($event)"
|
(fullscreenChanged)="onFullscreenChanged($event)"
|
||||||
|
|||||||
@ -17,17 +17,21 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component, ElementRef,
|
||||||
EventEmitter,
|
EventEmitter, HostBinding, Inject,
|
||||||
Input,
|
Input, OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
Output
|
Output, Renderer2, ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { PageComponent } from '@shared/components/page.component';
|
import { PageComponent } from '@shared/components/page.component';
|
||||||
import { DashboardWidget, DashboardWidgets } from '@home/models/dashboard-component.models';
|
import { DashboardWidget, DashboardWidgets } from '@home/models/dashboard-component.models';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppState } from '@core/core.state';
|
import { AppState } from '@core/core.state';
|
||||||
import { SafeStyle } from '@angular/platform-browser';
|
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 {
|
export enum WidgetComponentActionType {
|
||||||
MOUSE_DOWN,
|
MOUSE_DOWN,
|
||||||
@ -43,13 +47,23 @@ export class WidgetComponentAction {
|
|||||||
actionType: WidgetComponentActionType;
|
actionType: WidgetComponentActionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @dynamic
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-widget-container',
|
selector: 'tb-widget-container',
|
||||||
templateUrl: './widget-container.component.html',
|
templateUrl: './widget-container.component.html',
|
||||||
styleUrls: ['./widget-container.component.scss'],
|
styleUrls: ['./widget-container.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
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()
|
@Input()
|
||||||
widget: DashboardWidget;
|
widget: DashboardWidget;
|
||||||
@ -87,13 +101,35 @@ export class WidgetContainerComponent extends PageComponent implements OnInit {
|
|||||||
@Output()
|
@Output()
|
||||||
widgetComponentAction: EventEmitter<WidgetComponentAction> = new EventEmitter<WidgetComponentAction>();
|
widgetComponentAction: EventEmitter<WidgetComponentAction> = new EventEmitter<WidgetComponentAction>();
|
||||||
|
|
||||||
|
private cssClass: string;
|
||||||
|
|
||||||
constructor(protected store: Store<AppState>,
|
constructor(protected store: Store<AppState>,
|
||||||
private cd: ChangeDetectorRef) {
|
private cd: ChangeDetectorRef,
|
||||||
|
private renderer: Renderer2,
|
||||||
|
@Inject(DOCUMENT) private document: Document) {
|
||||||
super(store);
|
super(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.widget.widgetContext.containerChangeDetector = this.cd;
|
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) {
|
isHighlighted(widget: DashboardWidget) {
|
||||||
@ -105,6 +141,11 @@ export class WidgetContainerComponent extends PageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onFullscreenChanged(expanded: boolean) {
|
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);
|
this.widgetFullscreenChanged.emit(expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -65,7 +65,7 @@ import {
|
|||||||
validateEntityId
|
validateEntityId
|
||||||
} from '@core/utils';
|
} from '@core/utils';
|
||||||
import {
|
import {
|
||||||
IDynamicWidgetComponent, ShowWidgetHeaderActionFunction,
|
IDynamicWidgetComponent, ShowWidgetHeaderActionFunction, updateEntityParams,
|
||||||
WidgetContext,
|
WidgetContext,
|
||||||
WidgetHeaderAction,
|
WidgetHeaderAction,
|
||||||
WidgetInfo,
|
WidgetInfo,
|
||||||
@ -1070,7 +1070,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
|
|||||||
case WidgetActionType.updateDashboardState:
|
case WidgetActionType.updateDashboardState:
|
||||||
let targetDashboardStateId = descriptor.targetDashboardStateId;
|
let targetDashboardStateId = descriptor.targetDashboardStateId;
|
||||||
const params = deepClone(this.widgetContext.stateController.getStateParams());
|
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 (type === WidgetActionType.openDashboardState) {
|
||||||
if (descriptor.openInPopover) {
|
if (descriptor.openInPopover) {
|
||||||
this.openDashboardStateInPopover($event, descriptor.targetDashboardStateId, params,
|
this.openDashboardStateInPopover($event, descriptor.targetDashboardStateId, params,
|
||||||
@ -1091,7 +1091,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
|
|||||||
targetDashboardStateId = descriptor.targetDashboardStateId;
|
targetDashboardStateId = descriptor.targetDashboardStateId;
|
||||||
const stateObject: StateObject = {};
|
const stateObject: StateObject = {};
|
||||||
stateObject.params = {};
|
stateObject.params = {};
|
||||||
this.updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName, entityLabel);
|
updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName, entityLabel);
|
||||||
if (targetDashboardStateId) {
|
if (targetDashboardStateId) {
|
||||||
stateObject.id = targetDashboardStateId;
|
stateObject.id = targetDashboardStateId;
|
||||||
}
|
}
|
||||||
@ -1360,7 +1360,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
|
|||||||
this.widgetContentContainer, this.dashboardPageComponent, preferredPlacement, hideOnClickOutside,
|
this.widgetContentContainer, this.dashboardPageComponent, preferredPlacement, hideOnClickOutside,
|
||||||
injector,
|
injector,
|
||||||
{
|
{
|
||||||
embed: true,
|
embedded: true,
|
||||||
syncStateWithQueryParam: false,
|
syncStateWithQueryParam: false,
|
||||||
hideToolbar: hideDashboardToolbar,
|
hideToolbar: hideDashboardToolbar,
|
||||||
currentState: objToBase64([stateObject]),
|
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> {
|
private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array<WidgetResource>): Observable<any> {
|
||||||
if (isDefined(customCss) && customCss.length > 0) {
|
if (isDefined(customCss) && customCss.length > 0) {
|
||||||
this.cssParser.cssPreviewNamespace = actionNamespace;
|
this.cssParser.cssPreviewNamespace = actionNamespace;
|
||||||
|
|||||||
@ -37,7 +37,7 @@ import {
|
|||||||
IStateController,
|
IStateController,
|
||||||
IWidgetSubscription,
|
IWidgetSubscription,
|
||||||
IWidgetUtils,
|
IWidgetUtils,
|
||||||
RpcApi,
|
RpcApi, StateParams,
|
||||||
SubscriptionEntityInfo,
|
SubscriptionEntityInfo,
|
||||||
TimewindowFunctions,
|
TimewindowFunctions,
|
||||||
WidgetActionsApi,
|
WidgetActionsApi,
|
||||||
@ -81,6 +81,7 @@ import { Router } from '@angular/router';
|
|||||||
import { catchError, map, mergeMap, switchMap } from 'rxjs/operators';
|
import { catchError, map, mergeMap, switchMap } from 'rxjs/operators';
|
||||||
import { FormattedData } from '@home/components/widget/lib/maps/map-models';
|
import { FormattedData } from '@home/components/widget/lib/maps/map-models';
|
||||||
import { TbPopoverComponent } from '@shared/components/popover.component';
|
import { TbPopoverComponent } from '@shared/components/popover.component';
|
||||||
|
import { EntityId } from '@shared/models/id/entity-id';
|
||||||
|
|
||||||
export interface IWidgetAction {
|
export interface IWidgetAction {
|
||||||
name: string;
|
name: string;
|
||||||
@ -545,3 +546,27 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
|
|||||||
descriptor
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
41
ui-ngx/src/app/shared/components/css.component.html
Normal file
41
ui-ngx/src/app/shared/components/css.component.html
Normal 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>
|
||||||
66
ui-ngx/src/app/shared/components/css.component.scss
Normal file
66
ui-ngx/src/app/shared/components/css.component.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
207
ui-ngx/src/app/shared/components/css.component.ts
Normal file
207
ui-ngx/src/app/shared/components/css.component.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -47,6 +47,10 @@ export class TbMarkdownComponent implements OnChanges {
|
|||||||
|
|
||||||
@Input() data: string | undefined;
|
@Input() data: string | undefined;
|
||||||
|
|
||||||
|
@Input() context: any;
|
||||||
|
|
||||||
|
@Input() additionalCompileModules: Type<any>[];
|
||||||
|
|
||||||
@Input() markdownClass: string | undefined;
|
@Input() markdownClass: string | undefined;
|
||||||
|
|
||||||
@Input() style: { [klass: string]: any } = {};
|
@Input() style: { [klass: string]: any } = {};
|
||||||
@ -94,6 +98,10 @@ export class TbMarkdownComponent implements OnChanges {
|
|||||||
this.markdownContainer.clear();
|
this.markdownContainer.clear();
|
||||||
const parent = this;
|
const parent = this;
|
||||||
let readyObservable: Observable<void>;
|
let readyObservable: Observable<void>;
|
||||||
|
let compileModules = [this.sharedModule];
|
||||||
|
if (this.additionalCompileModules) {
|
||||||
|
compileModules = compileModules.concat(this.additionalCompileModules);
|
||||||
|
}
|
||||||
this.dynamicComponentFactoryService.createDynamicComponentFactory(
|
this.dynamicComponentFactoryService.createDynamicComponentFactory(
|
||||||
class TbMarkdownInstance {
|
class TbMarkdownInstance {
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@ -101,7 +109,7 @@ export class TbMarkdownComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
template,
|
template,
|
||||||
[this.sharedModule],
|
compileModules,
|
||||||
true
|
true
|
||||||
).subscribe((factory) => {
|
).subscribe((factory) => {
|
||||||
this.tbMarkdownInstanceComponentFactory = factory;
|
this.tbMarkdownInstanceComponentFactory = factory;
|
||||||
@ -109,6 +117,11 @@ export class TbMarkdownComponent implements OnChanges {
|
|||||||
try {
|
try {
|
||||||
this.tbMarkdownInstanceComponentRef =
|
this.tbMarkdownInstanceComponentRef =
|
||||||
this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector);
|
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.tbMarkdownInstanceComponentRef.instance.style = this.style;
|
||||||
this.handlePlugins(this.tbMarkdownInstanceComponentRef.location.nativeElement);
|
this.handlePlugins(this.tbMarkdownInstanceComponentRef.location.nativeElement);
|
||||||
this.markdownService.highlight(this.tbMarkdownInstanceComponentRef.location.nativeElement);
|
this.markdownService.highlight(this.tbMarkdownInstanceComponentRef.location.nativeElement);
|
||||||
|
|||||||
@ -97,6 +97,7 @@ export interface DashboardSettings {
|
|||||||
toolbarAlwaysOpen?: boolean;
|
toolbarAlwaysOpen?: boolean;
|
||||||
hideToolbar?: boolean;
|
hideToolbar?: boolean;
|
||||||
titleColor?: string;
|
titleColor?: string;
|
||||||
|
dashboardCss?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardConfiguration {
|
export interface DashboardConfiguration {
|
||||||
|
|||||||
@ -506,6 +506,7 @@ export interface WidgetConfig {
|
|||||||
padding?: string;
|
padding?: string;
|
||||||
margin?: string;
|
margin?: string;
|
||||||
widgetStyle?: {[klass: string]: any};
|
widgetStyle?: {[klass: string]: any};
|
||||||
|
widgetCss?: string;
|
||||||
titleStyle?: {[klass: string]: any};
|
titleStyle?: {[klass: string]: any};
|
||||||
units?: string;
|
units?: string;
|
||||||
decimals?: number;
|
decimals?: number;
|
||||||
|
|||||||
@ -156,6 +156,7 @@ import { TbPopoverService } from '@shared/components/popover.service';
|
|||||||
import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens';
|
import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens';
|
||||||
import { TbMarkdownComponent } from '@shared/components/markdown.component';
|
import { TbMarkdownComponent } from '@shared/components/markdown.component';
|
||||||
import { ProtobufContentComponent } from '@shared/components/protobuf-content.component';
|
import { ProtobufContentComponent } from '@shared/components/protobuf-content.component';
|
||||||
|
import { CssComponent } from '@shared/components/css.component';
|
||||||
|
|
||||||
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
|
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
|
||||||
return markedOptionsService;
|
return markedOptionsService;
|
||||||
@ -233,6 +234,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
|||||||
JsonObjectEditComponent,
|
JsonObjectEditComponent,
|
||||||
JsonContentComponent,
|
JsonContentComponent,
|
||||||
JsFuncComponent,
|
JsFuncComponent,
|
||||||
|
CssComponent,
|
||||||
FabTriggerDirective,
|
FabTriggerDirective,
|
||||||
FabActionsDirective,
|
FabActionsDirective,
|
||||||
FabToolbarComponent,
|
FabToolbarComponent,
|
||||||
@ -378,6 +380,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
|||||||
JsonObjectEditComponent,
|
JsonObjectEditComponent,
|
||||||
JsonContentComponent,
|
JsonContentComponent,
|
||||||
JsFuncComponent,
|
JsFuncComponent,
|
||||||
|
CssComponent,
|
||||||
FabTriggerDirective,
|
FabTriggerDirective,
|
||||||
FabActionsDirective,
|
FabActionsDirective,
|
||||||
FabToolbarComponent,
|
FabToolbarComponent,
|
||||||
|
|||||||
@ -811,6 +811,8 @@
|
|||||||
"dashboard-logo-settings": "Dashboard logo settings",
|
"dashboard-logo-settings": "Dashboard logo settings",
|
||||||
"display-dashboard-logo": "Display logo in dashboard fullscreen mode",
|
"display-dashboard-logo": "Display logo in dashboard fullscreen mode",
|
||||||
"dashboard-logo-image": "Dashboard logo image",
|
"dashboard-logo-image": "Dashboard logo image",
|
||||||
|
"advanced-settings": "Advanced settings",
|
||||||
|
"dashboard-css": "Dashboard CSS",
|
||||||
"import": "Import dashboard",
|
"import": "Import dashboard",
|
||||||
"export": "Export dashboard",
|
"export": "Export dashboard",
|
||||||
"export-failed-error": "Unable to export dashboard: {{error}}",
|
"export-failed-error": "Unable to export dashboard: {{error}}",
|
||||||
@ -3084,6 +3086,7 @@
|
|||||||
"padding": "Padding",
|
"padding": "Padding",
|
||||||
"margin": "Margin",
|
"margin": "Margin",
|
||||||
"widget-style": "Widget style",
|
"widget-style": "Widget style",
|
||||||
|
"widget-css": "Widget CSS",
|
||||||
"title-style": "Title style",
|
"title-style": "Title style",
|
||||||
"mobile-mode-settings": "Mobile mode",
|
"mobile-mode-settings": "Mobile mode",
|
||||||
"order": "Order",
|
"order": "Order",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user