widget-config.card-style
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts
index 7e4f1d1d96..a1a3e951f2 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts
@@ -15,12 +15,13 @@
///
import { Component } from '@angular/core';
-import { TargetDevice, WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
+import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
- singleSwitchDefaultSettings, singleSwitchLayoutImages,
+ singleSwitchDefaultSettings,
+ singleSwitchLayoutImages,
singleSwitchLayouts,
singleSwitchLayoutTranslations
} from '@home/components/widget/lib/rpc/single-switch-widget.models';
@@ -34,7 +35,11 @@ import { ValueType } from '@shared/models/constants';
export class SingleSwitchWidgetSettingsComponent extends WidgetSettingsComponent {
get targetDevice(): TargetDevice {
- return this.widget?.config?.targetDevice;
+ return this.widgetConfig?.config?.targetDevice;
+ }
+
+ get widgetType(): widgetType {
+ return this.widgetConfig?.widgetType;
}
singleSwitchLayouts = singleSwitchLayouts;
@@ -64,6 +69,7 @@ export class SingleSwitchWidgetSettingsComponent extends WidgetSettingsComponent
initialState: [settings.initialState, []],
onUpdateState: [settings.onUpdateState, []],
offUpdateState: [settings.offUpdateState, []],
+ disabledState: [settings.disabledState, []],
layout: [settings.layout, []],
autoScale: [settings.autoScale, []],
@@ -104,7 +110,7 @@ export class SingleSwitchWidgetSettingsComponent extends WidgetSettingsComponent
return ['showLabel', 'showIcon', 'showOnLabel', 'showOffLabel'];
}
- protected updateValidators(emitEvent: boolean): void {
+ protected updateValidators(_emitEvent: boolean): void {
const showLabel: boolean = this.singleSwitchWidgetSettingsForm.get('showLabel').value;
const showIcon: boolean = this.singleSwitchWidgetSettingsForm.get('showIcon').value;
const showOnLabel: boolean = this.singleSwitchWidgetSettingsForm.get('showOnLabel').value;
@@ -152,6 +158,4 @@ export class SingleSwitchWidgetSettingsComponent extends WidgetSettingsComponent
this.singleSwitchWidgetSettingsForm.get('offLabelColor').disable();
}
}
-
- protected readonly ValueType = ValueType;
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts
index d14d0fd594..43098a011b 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts
@@ -37,7 +37,7 @@ export class SlideToggleWidgetSettingsComponent extends WidgetSettingsComponent
}
get targetDevice(): TargetDevice {
- return this.widget?.config?.targetDevice;
+ return this.widgetConfig?.config?.targetDevice;
}
protected settingsForm(): UntypedFormGroup {
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts
index 197fc2d229..e74275b659 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts
@@ -37,7 +37,7 @@ export class SwitchControlWidgetSettingsComponent extends WidgetSettingsComponen
}
get targetDevice(): TargetDevice {
- return this.widget?.config?.targetDevice;
+ return this.widgetConfig?.config?.targetDevice;
}
protected settingsForm(): UntypedFormGroup {
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
index 30c1b748de..607cb91318 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
@@ -15,7 +15,9 @@
///
import { NgModule, Type } from '@angular/core';
-import { QrCodeWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/qrcode-widget-settings.component';
+import {
+ QrCodeWidgetSettingsComponent
+} from '@home/components/widget/lib/settings/cards/qrcode-widget-settings.component';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module';
@@ -33,7 +35,9 @@ import {
MarkdownWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/markdown-widget-settings.component';
import { LabelWidgetLabelComponent } from '@home/components/widget/lib/settings/cards/label-widget-label.component';
-import { LabelWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/label-widget-settings.component';
+import {
+ LabelWidgetSettingsComponent
+} from '@home/components/widget/lib/settings/cards/label-widget-settings.component';
import {
SimpleCardWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/simple-card-widget-settings.component';
@@ -311,6 +315,9 @@ import {
import {
SingleSwitchWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/single-switch-widget-settings.component';
+import {
+ ActionButtonWidgetSettingsComponent
+} from '@home/components/widget/lib/settings/button/action-button-widget-settings.component';
@NgModule({
declarations: [
@@ -424,7 +431,8 @@ import {
DoughnutWidgetSettingsComponent,
RangeChartWidgetSettingsComponent,
BarChartWithLabelsWidgetSettingsComponent,
- SingleSwitchWidgetSettingsComponent
+ SingleSwitchWidgetSettingsComponent,
+ ActionButtonWidgetSettingsComponent
],
imports: [
CommonModule,
@@ -543,7 +551,8 @@ import {
DoughnutWidgetSettingsComponent,
RangeChartWidgetSettingsComponent,
BarChartWithLabelsWidgetSettingsComponent,
- SingleSwitchWidgetSettingsComponent
+ SingleSwitchWidgetSettingsComponent,
+ ActionButtonWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@@ -629,5 +638,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
index 124b6c4d99..3143ab1ee2 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
@@ -26,6 +26,13 @@
outline: none;
transition: all .2s ease-in-out;
+
+ &.tb-overflow-visible {
+ overflow: visible;
+ .tb-widget {
+ overflow: visible;
+ }
+ }
}
div.tb-widget {
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
index f4b54a395d..81c206391a 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
@@ -22,7 +22,6 @@ import {
ElementRef,
EventEmitter,
HostBinding,
- Inject,
Input,
OnDestroy,
OnInit,
@@ -36,10 +35,9 @@ import { DashboardWidget, DashboardWidgets } from '@home/models/dashboard-compon
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { SafeStyle } from '@angular/platform-browser';
-import { guid, isNotEmptyStr } from '@core/utils';
-import cssjs from '@core/css/css';
-import { DOCUMENT } from '@angular/common';
+import { isNotEmptyStr } from '@core/utils';
import { GridsterItemComponent } from 'angular-gridster2';
+import { UtilsService } from '@core/services/utils.service';
export enum WidgetComponentActionType {
MOUSE_DOWN,
@@ -86,6 +84,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A
@Input()
isEdit: boolean;
+ @Input()
+ isPreview: boolean;
+
@Input()
isMobile: boolean;
@@ -115,7 +116,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A
constructor(protected store: Store,
private cd: ChangeDetectorRef,
private renderer: Renderer2,
- @Inject(DOCUMENT) private document: Document) {
+ private utils: UtilsService) {
super(store);
}
@@ -123,12 +124,8 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A
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);
+ this.cssClass =
+ this.utils.applyCssToElement(this.renderer, this.gridsterItem.el, 'tb-widget-css', cssString);
}
}
@@ -138,10 +135,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A
ngOnDestroy(): void {
if (this.cssClass) {
- const el = this.document.getElementById(this.cssClass);
- if (el) {
- el.parentNode.removeChild(el);
- }
+ this.utils.clearCssElement(this.renderer, this.cssClass);
}
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html
index 38c4527074..3a9ced83de 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html
@@ -25,6 +25,7 @@
[autofillHeight]="true"
[columns]="24"
[isEdit]="false"
+ [isPreview]="true"
[isMobileDisabled]="true"
[isEditActionEnabled]="false"
[isRemoveActionEnabled]="false">
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 1c029b6524..15e982c060 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
@@ -131,6 +131,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
@Input()
isEdit: boolean;
+ @Input()
+ isPreview: boolean;
+
@Input()
isMobile: boolean;
@@ -231,6 +234,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetContext.store = this.store;
this.widgetContext.servicesMap = ServicesMap;
this.widgetContext.isEdit = this.isEdit;
+ this.widgetContext.isPreview = this.isPreview;
this.widgetContext.isMobile = this.isMobile;
this.widgetContext.toastTargetId = this.toastTargetId;
@@ -252,6 +256,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
handleWidgetAction: this.handleWidgetAction.bind(this),
elementClick: this.elementClick.bind(this),
cardClick: this.cardClick.bind(this),
+ click: this.click.bind(this),
getActiveEntityInfo: this.getActiveEntityInfo.bind(this),
openDashboardStateInSeparateDialog: this.openDashboardStateInSeparateDialog.bind(this),
openDashboardStateInPopover: this.openDashboardStateInPopover.bind(this)
@@ -411,6 +416,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetType = this.widgetInfo.widgetTypeFunction;
this.typeParameters = this.widgetInfo.typeParameters;
this.widgetContext.embedTitlePanel = this.typeParameters.embedTitlePanel;
+ this.widgetContext.overflowVisible = this.typeParameters.overflowVisible;
if (!this.widgetType) {
this.widgetTypeInstance = {};
@@ -1423,7 +1429,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
private cardClick($event: Event) {
- const descriptors = this.getActionDescriptors('cardClick');
+ this.onClick($event, 'cardClick');
+ }
+
+ private click($event: Event) {
+ this.onClick($event, 'click');
+ }
+
+ private onClick($event: Event, sourceId: string) {
+ const descriptors = this.getActionDescriptors(sourceId);
if (descriptors.length) {
$event.stopPropagation();
const descriptor = descriptors[0];
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 4a7fc32502..a834120dc6 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
@@ -263,6 +263,7 @@ export class WidgetContext {
height: number;
$scope: IDynamicWidgetComponent;
isEdit: boolean;
+ isPreview: boolean;
isMobile: boolean;
toastTargetId: string;
@@ -279,6 +280,7 @@ export class WidgetContext {
timeWindow?: WidgetTimewindow;
embedTitlePanel?: boolean;
+ overflowVisible?: boolean;
hideTitlePanel = false;
diff --git a/ui-ngx/src/app/shared/components/button/widget-button.component.html b/ui-ngx/src/app/shared/components/button/widget-button.component.html
new file mode 100644
index 0000000000..9b239db068
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/button/widget-button.component.html
@@ -0,0 +1,39 @@
+
+
diff --git a/ui-ngx/src/app/shared/components/button/widget-button.component.scss b/ui-ngx/src/app/shared/components/button/widget-button.component.scss
new file mode 100644
index 0000000000..a0abc95731
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/button/widget-button.component.scss
@@ -0,0 +1,189 @@
+/**
+ * Copyright © 2016-2024 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.
+ */
+$defaultMainColor: #3F52DD;
+$defaultBackgroundColor: #FFFFFF;
+$defaultBoxShadowColor: rgba(0, 0, 0, 0.08);
+$defaultDisabledBoxShadowColor: rgba(0, 0, 0, 0);
+
+$defaultMainColorDisabled: rgba(0, 0, 0, 0.38);
+$defaultBackgroundColorDisabled: rgba(0, 0, 0, 0.03);
+
+$mainColorEnabled: var(--tb-widget-button-main-color-enabled, $defaultMainColor);
+$backgroundColorEnabled: var(--tb-widget-button-background-color-enabled, $defaultBackgroundColor);
+$boxShadowColorEnabled: var(--tb-widget-button-box-shadow-color-enabled, $defaultBoxShadowColor);
+
+$mainColorHovered: var(--tb-widget-button-main-color-hovered, $defaultMainColor);
+$backgroundColorHovered: var(--tb-widget-button-background-color-hovered, $defaultBackgroundColor);
+$boxShadowColorHovered: var(--tb-widget-button-box-shadow-color-hovered, $defaultBoxShadowColor);
+$mainColorHoveredFilled: var(--tb-widget-button-main-color-hovered-filled, #3347db); // main.darken(6)
+
+$mainColorPressed: var(--tb-widget-button-main-color-pressed, $defaultMainColor);
+$backgroundColorPressed: var(--tb-widget-button-background-color-pressed, $defaultBackgroundColor);
+$boxShadowColorPressed: var(--tb-widget-button-box-shadow-color-pressed, $defaultBoxShadowColor);
+$mainColorPressedFilled: var(--tb-widget-button-main-color-pressed-filled, #273cd9); // main.darken(12)
+$mainColorPressedRipple: var(--tb-widget-button-main-color-pressed-ripple, rgba(63, 82, 221, 0.1)); // Alpha(Main, 0.1)
+$mainColorPressedRippleFilled: var(--tb-widget-button-main-color-pressed-ripple-filled, #2439cd); // main.darken(18)
+
+$mainColorActivated: var(--tb-widget-button-main-color-activated, $defaultMainColor);
+$backgroundColorActivated: var(--tb-widget-button-background-color-activated, $defaultBackgroundColor);
+$boxShadowColorActivated: var(--tb-widget-button-box-shadow-color-activated, $defaultBoxShadowColor);
+$mainColorActivatedFilled: var(--tb-widget-button-main-color-activated-filled, #273cd9); // main.darken(12)
+
+$mainColorDisabled: var(--tb-widget-button-main-color-disabled, $defaultMainColorDisabled);
+$backgroundColorDisabled: var(--tb-widget-button-background-color-disabled, $defaultBackgroundColorDisabled);
+$boxShadowColorDisabled: var(--tb-widget-button-box-shadow-color-disabled, $defaultBoxShadowColor);
+
+
+@mixin _tb-widget-button-styles($main, $background, $boxShadow) {
+ color: $main;
+ background-color: $background;
+ box-shadow: 0 4px 8px 0 $boxShadow;
+ &.tb-outlined {
+ border: 1px solid $main;
+ }
+ &.tb-filled {
+ color: $background;
+ background-color: $main;
+ }
+ &.tb-underlined {
+ border-bottom: 2px solid $main;
+ }
+ &.tb-basic {
+ background-color: transparent;
+ }
+}
+
+
+.mat-mdc-button.mat-mdc-button-base.tb-widget-button {
+ width: 100%;
+ height: 100%;
+ padding: 8px 12px;
+ .mdc-button__label {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ .tb-widget-button-content {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ gap: 4px;
+ justify-content: center;
+ align-items: center;
+ .mat-icon {
+ margin: 0;
+ }
+ span.tb-widget-button-label {
+ line-height: normal;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ .mat-mdc-button-persistent-ripple::before {
+ opacity: 0;
+ }
+
+ @include _tb-widget-button-styles($mainColorEnabled, $backgroundColorEnabled, $boxShadowColorEnabled);
+
+ &:not(:disabled):not(.tb-disabled-state) {
+ &:hover, &.tb-hover-state {
+ &:not(:active):not(.tb-active-state) {
+ &:not(.tb-filled) {
+ .mat-mdc-button-persistent-ripple::before {
+ opacity: 0.04;
+ background-color: $mainColorHovered;
+ }
+ }
+ &.tb-filled {
+ .mat-mdc-button-persistent-ripple::before {
+ opacity: 1;
+ background-color: $mainColorHoveredFilled;
+ }
+ }
+ @include _tb-widget-button-styles($mainColorHovered, $backgroundColorHovered, $boxShadowColorHovered);
+ }
+ }
+ &.tb-pressed-state {
+ &:not(.tb-filled) {
+ .mat-mdc-button-ripple {
+ background-color: $mainColorPressedRipple;
+ }
+ }
+ &.tb-filled {
+ .mat-mdc-button-ripple {
+ background-color: $mainColorPressedRippleFilled;
+ }
+ }
+ }
+ &.tb-pressed {
+ &:not(.tb-filled) {
+ .mat-ripple-element {
+ background-color: $mainColorPressedRipple;
+ }
+ }
+ &.tb-filled {
+ .mat-ripple-element {
+ background-color: $mainColorPressedRippleFilled;
+ }
+ }
+ }
+ &.tb-pressed, &.tb-pressed-state {
+ &:not(.tb-filled) {
+ .mat-mdc-button-persistent-ripple::before {
+ opacity: 0.12;
+ background-color: $mainColorPressed;
+ }
+ }
+ &.tb-filled {
+ .mat-mdc-button-persistent-ripple::before {
+ opacity: 1;
+ background-color: $mainColorPressedFilled;
+ }
+ }
+ @include _tb-widget-button-styles($mainColorPressed, $backgroundColorPressed, $boxShadowColorPressed);
+ }
+ &:active, &.tb-active-state {
+ &:not(.tb-pressed):not(.tb-pressed-state) {
+ &:not(.tb-filled) {
+ .mat-mdc-button-persistent-ripple::before {
+ opacity: 0.12;
+ background-color: $mainColorActivated;
+ }
+ }
+ &.tb-filled {
+ .mat-mdc-button-persistent-ripple::before {
+ opacity: 1;
+ background-color: $mainColorActivatedFilled;
+ }
+ }
+ @include _tb-widget-button-styles($mainColorActivated, $backgroundColorActivated, $boxShadowColorActivated);
+ }
+ }
+ }
+
+ &:disabled, &.tb-disabled-state {
+ &:not(.tb-filled) {
+ @include _tb-widget-button-styles($mainColorDisabled, $backgroundColorDisabled, $boxShadowColorDisabled);
+ }
+ &.tb-filled {
+ @include _tb-widget-button-styles($backgroundColorDisabled, $mainColorDisabled, $boxShadowColorDisabled);
+ }
+ }
+}
diff --git a/ui-ngx/src/app/shared/components/button/widget-button.component.ts b/ui-ngx/src/app/shared/components/button/widget-button.component.ts
new file mode 100644
index 0000000000..d8fe88b48e
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/button/widget-button.component.ts
@@ -0,0 +1,178 @@
+///
+/// Copyright © 2016-2024 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 {
+ AfterViewInit,
+ Component,
+ ElementRef,
+ EventEmitter,
+ Input,
+ OnChanges,
+ OnDestroy,
+ OnInit,
+ Output,
+ Renderer2,
+ SimpleChanges, ViewChild,
+ ViewEncapsulation
+} from '@angular/core';
+import {
+ generateWidgetButtonAppearanceCss,
+ widgetButtonDefaultAppearance
+} from '@shared/components/button/widget-button.models';
+import { coerceBoolean } from '@shared/decorators/coercion';
+import { ComponentStyle, iconStyle } from '@shared/models/widget-settings.models';
+import { UtilsService } from '@core/services/utils.service';
+import { ResizeObserver } from '@juggle/resize-observer';
+
+const initialButtonHeight = 60;
+const horizontalLayoutPadding = 24;
+const verticalLayoutPadding = 16;
+
+@Component({
+ selector: 'tb-widget-button',
+ templateUrl: './widget-button.component.html',
+ styleUrls: ['./widget-button.component.scss'],
+ encapsulation: ViewEncapsulation.None
+})
+export class WidgetButtonComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges {
+
+ @ViewChild('widgetButton', {read: ElementRef})
+ widgetButton: ElementRef;
+
+ @ViewChild('widgetButtonContent', {static: false})
+ widgetButtonContent: ElementRef;
+
+ @Input()
+ appearance = widgetButtonDefaultAppearance;
+
+ @Input()
+ borderRadius = '4px';
+
+ @Input()
+ @coerceBoolean()
+ disabled = false;
+
+ @Input()
+ @coerceBoolean()
+ activated = false;
+
+ @Input()
+ @coerceBoolean()
+ hovered = false;
+
+ @Input()
+ @coerceBoolean()
+ pressed = false;
+
+ @Input()
+ @coerceBoolean()
+ disableEvents = false;
+
+ @Output()
+ clicked = new EventEmitter();
+
+ iconStyle: ComponentStyle = {};
+
+ mousePressed = false;
+
+ private buttonResize$: ResizeObserver;
+
+ private appearanceCssClass: string;
+
+ constructor(private renderer: Renderer2,
+ private elementRef: ElementRef,
+ private utils: UtilsService) {}
+
+ ngOnInit(): void {
+ this.updateAppearance();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ for (const propName of Object.keys(changes)) {
+ const change = changes[propName];
+ if (!change.firstChange) {
+ if (propName === 'appearance') {
+ this.updateAppearance();
+ }
+ }
+ }
+ }
+
+ ngAfterViewInit(): void {
+ this.updateAutoScale();
+ }
+
+ ngOnDestroy(): void {
+ if (this.buttonResize$) {
+ this.buttonResize$.disconnect();
+ }
+ this.clearAppearanceCss();
+ }
+
+ private updateAppearance(): void {
+ this.clearAppearanceCss();
+ if (this.appearance.showIcon) {
+ this.iconStyle = iconStyle(this.appearance.iconSize, this.appearance.iconSizeUnit);
+ }
+ const appearanceCss = generateWidgetButtonAppearanceCss(this.appearance);
+ this.appearanceCssClass = this.utils.applyCssToElement(this.renderer, this.elementRef.nativeElement,
+ 'tb-widget-button', appearanceCss);
+ this.updateAutoScale();
+ }
+
+ private clearAppearanceCss(): void {
+ if (this.appearanceCssClass) {
+ this.utils.clearCssElement(this.renderer, this.appearanceCssClass, this.elementRef?.nativeElement);
+ this.appearanceCssClass = null;
+ }
+ }
+
+ private updateAutoScale() {
+ if (this.buttonResize$) {
+ this.buttonResize$.disconnect();
+ }
+ if (this.widgetButton && this.widgetButtonContent) {
+ if (this.appearance.autoScale) {
+ this.buttonResize$ = new ResizeObserver(() => {
+ this.onResize();
+ });
+ this.buttonResize$.observe(this.widgetButton.nativeElement);
+ this.onResize();
+ } else {
+ this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'transform', 'none');
+ this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'width', '100%');
+ }
+ }
+ }
+
+ private onResize() {
+ const height = this.widgetButton.nativeElement.getBoundingClientRect().height;
+ const buttonScale = height / initialButtonHeight;
+ const paddingScale = Math.min(buttonScale, 1);
+ const buttonWidth = this.widgetButton.nativeElement.getBoundingClientRect().width - (horizontalLayoutPadding * paddingScale);
+ const buttonHeight = this.widgetButton.nativeElement.getBoundingClientRect().height - (verticalLayoutPadding * paddingScale);
+ this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'transform', `scale(1)`);
+ this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'width', 'auto');
+ const contentWidth = this.widgetButtonContent.nativeElement.getBoundingClientRect().width;
+ const contentHeight = this.widgetButtonContent.nativeElement.getBoundingClientRect().height;
+ const maxScale = Math.max(1, buttonScale);
+ const scale = Math.min(Math.min(buttonWidth / contentWidth, buttonHeight / contentHeight), maxScale);
+ const targetWidth = buttonWidth / scale;
+ this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'width', targetWidth + 'px');
+ this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'transform', `scale(${scale})`);
+ }
+
+}
diff --git a/ui-ngx/src/app/shared/components/button/widget-button.models.ts b/ui-ngx/src/app/shared/components/button/widget-button.models.ts
new file mode 100644
index 0000000000..34f13864a9
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/button/widget-button.models.ts
@@ -0,0 +1,271 @@
+///
+/// Copyright © 2016-2024 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 { cssUnit } from '@shared/models/widget-settings.models';
+import tinycolor from 'tinycolor2';
+
+const defaultMainColor = '#3F52DD';
+const defaultBackgroundColor = '#FFFFFF';
+
+const hoveredFilledDarkenAmount = 6;
+const pressedFilledDarkenAmount = 12;
+const activatedFilledDarkenAmount = 12;
+const pressedRippleFilledDarkenAmount = 18;
+
+export const defaultMainColorDisabled = 'rgba(0, 0, 0, 0.38)';
+export const defaultBackgroundColorDisabled = 'rgba(0, 0, 0, 0.03)';
+
+const defaultBoxShadowColor = 'rgba(0, 0, 0, 0.08)';
+const defaultDisabledBoxShadowColor = 'rgba(0, 0, 0, 0)';
+
+export enum WidgetButtonType {
+ outlined = 'outlined',
+ filled = 'filled',
+ underlined = 'underlined',
+ basic = 'basic'
+}
+
+export const widgetButtonTypes = Object.keys(WidgetButtonType) as WidgetButtonType[];
+
+export const widgetButtonTypeTranslations = new Map(
+ [
+ [WidgetButtonType.outlined, 'widgets.button.outlined'],
+ [WidgetButtonType.filled, 'widgets.button.filled'],
+ [WidgetButtonType.underlined, 'widgets.button.underlined'],
+ [WidgetButtonType.basic, 'widgets.button.basic']
+ ]
+);
+
+export const widgetButtonTypeImages = new Map(
+ [
+ [WidgetButtonType.outlined, 'assets/widget/button/outlined.svg'],
+ [WidgetButtonType.filled, 'assets/widget/button/filled.svg'],
+ [WidgetButtonType.underlined, 'assets/widget/button/underlined.svg'],
+ [WidgetButtonType.basic, 'assets/widget/button/basic.svg']
+ ]
+);
+
+export enum WidgetButtonState {
+ enabled = 'enabled',
+ hovered = 'hovered',
+ pressed = 'pressed',
+ activated = 'activated',
+ disabled = 'disabled'
+}
+
+export const widgetButtonStates = Object.keys(WidgetButtonState) as WidgetButtonState[];
+
+export const widgetButtonStatesTranslations = new Map(
+ [
+ [WidgetButtonState.enabled, 'widgets.button-state.enabled'],
+ [WidgetButtonState.hovered, 'widgets.button-state.hovered'],
+ [WidgetButtonState.pressed, 'widgets.button-state.pressed'],
+ [WidgetButtonState.activated, 'widgets.button-state.activated'],
+ [WidgetButtonState.disabled, 'widgets.button-state.disabled']
+ ]
+);
+
+export interface WidgetButtonCustomStyle {
+ overrideMainColor?: boolean;
+ mainColor?: string;
+ overrideBackgroundColor?: boolean;
+ backgroundColor?: string;
+ overrideDropShadow?: boolean;
+ dropShadow?: boolean;
+}
+
+export type WidgetButtonCustomStyles = Record;
+
+export interface WidgetButtonAppearance {
+ type: WidgetButtonType;
+ autoScale: boolean;
+ showLabel: boolean;
+ label: string;
+ showIcon: boolean;
+ icon: string;
+ iconSize: number;
+ iconSizeUnit: cssUnit;
+ mainColor: string;
+ backgroundColor: string;
+ customStyle: WidgetButtonCustomStyles;
+}
+
+export const widgetButtonDefaultAppearance: WidgetButtonAppearance = {
+ type: WidgetButtonType.outlined,
+ autoScale: true,
+ showLabel: true,
+ label: 'Button',
+ showIcon: true,
+ icon: 'home',
+ iconSize: 24,
+ iconSizeUnit: 'px',
+ mainColor: defaultMainColor,
+ backgroundColor: defaultBackgroundColor,
+ customStyle: {
+ enabled: null,
+ hovered: null,
+ pressed: null,
+ activated: null,
+ disabled: null
+ }
+};
+
+const mainColorVarPrefix = '--tb-widget-button-main-color-';
+const backgroundColorVarPrefix = '--tb-widget-button-background-color-';
+const boxShadowColorVarPrefix = '--tb-widget-button-box-shadow-color-';
+
+abstract class ButtonStateCssGenerator {
+
+ constructor() {}
+
+ public generateStateCss(appearance: WidgetButtonAppearance): string {
+ let mainColor = this.getMainColor(appearance);
+ let backgroundColor = this.getBackgroundColor(appearance);
+ const shadowEnabledByDefault = appearance.type !== WidgetButtonType.basic;
+ let shadowColor = shadowEnabledByDefault ? defaultBoxShadowColor : defaultDisabledBoxShadowColor;
+ const stateCustomStyle = appearance.customStyle[this.state];
+ if (stateCustomStyle?.overrideMainColor && stateCustomStyle?.mainColor) {
+ mainColor = stateCustomStyle.mainColor;
+ }
+ if (stateCustomStyle?.overrideBackgroundColor && stateCustomStyle?.backgroundColor) {
+ backgroundColor = stateCustomStyle.backgroundColor;
+ }
+ if (stateCustomStyle?.overrideDropShadow) {
+ shadowColor = !!stateCustomStyle.dropShadow ? defaultBoxShadowColor : defaultDisabledBoxShadowColor;
+ }
+
+ let css = `${mainColorVarPrefix}${this.state}: ${mainColor};\n`+
+ `${backgroundColorVarPrefix}${this.state}: ${backgroundColor};\n`+
+ `${boxShadowColorVarPrefix}${this.state}: ${shadowColor};`;
+ const additionalCss = this.generateAdditionalStateCss(mainColor, backgroundColor);
+ if (additionalCss) {
+ css += `\n${additionalCss}`;
+ }
+ return css;
+ }
+
+ protected abstract get state(): WidgetButtonState;
+
+ protected getMainColor(appearance: WidgetButtonAppearance): string {
+ return appearance.mainColor || defaultMainColor;
+ }
+
+ protected getBackgroundColor(appearance: WidgetButtonAppearance): string {
+ return appearance.backgroundColor || defaultBackgroundColor;
+ }
+
+ protected generateAdditionalStateCss(_mainColor: string, _backgroundColor: string): string {
+ return null;
+ }
+}
+
+class EnabledButtonStateCssGenerator extends ButtonStateCssGenerator {
+
+ protected get state(): WidgetButtonState {
+ return WidgetButtonState.enabled;
+ }
+}
+
+class HoveredButtonStateCssGenerator extends ButtonStateCssGenerator {
+
+ protected get state(): WidgetButtonState {
+ return WidgetButtonState.hovered;
+ }
+
+ protected generateAdditionalStateCss(mainColor: string): string {
+ const mainColorHoveredFilled = darkenColor(mainColor, hoveredFilledDarkenAmount);
+ return `--tb-widget-button-main-color-hovered-filled: ${mainColorHoveredFilled};`;
+ }
+}
+
+class PressedButtonStateCssGenerator extends ButtonStateCssGenerator {
+
+ protected get state(): WidgetButtonState {
+ return WidgetButtonState.pressed;
+ }
+
+ protected generateAdditionalStateCss(mainColor: string): string {
+ const mainColorPressedFilled = darkenColor(mainColor, pressedFilledDarkenAmount);
+ const mainColorInstance = tinycolor(mainColor);
+ const mainColorPressedRipple = mainColorInstance.setAlpha(mainColorInstance.getAlpha() * 0.1).toRgbString();
+ const mainColorPressedRippleFilled = darkenColor(mainColor, pressedRippleFilledDarkenAmount);
+ return `--tb-widget-button-main-color-pressed-filled: ${mainColorPressedFilled};\n`+
+ `--tb-widget-button-main-color-pressed-ripple: ${mainColorPressedRipple};\n`+
+ `--tb-widget-button-main-color-pressed-ripple-filled: ${mainColorPressedRippleFilled};`;
+ }
+}
+
+class ActivatedButtonStateCssGenerator extends ButtonStateCssGenerator {
+
+ protected get state(): WidgetButtonState {
+ return WidgetButtonState.activated;
+ }
+
+ protected generateAdditionalStateCss(mainColor: string): string {
+ const mainColorActivatedFilled = darkenColor(mainColor, activatedFilledDarkenAmount);
+ return `--tb-widget-button-main-color-activated-filled: ${mainColorActivatedFilled};`;
+ }
+}
+
+class DisabledButtonStateCssGenerator extends ButtonStateCssGenerator {
+
+ protected get state(): WidgetButtonState {
+ return WidgetButtonState.disabled;
+ }
+
+ protected getMainColor(): string {
+ return defaultMainColorDisabled;
+ }
+
+ protected getBackgroundColor(): string {
+ return defaultBackgroundColorDisabled;
+ }
+}
+
+const buttonStateCssGeneratorsMap = new Map(
+ [
+ [WidgetButtonState.enabled, new EnabledButtonStateCssGenerator()],
+ [WidgetButtonState.hovered, new HoveredButtonStateCssGenerator()],
+ [WidgetButtonState.pressed, new PressedButtonStateCssGenerator()],
+ [WidgetButtonState.activated, new ActivatedButtonStateCssGenerator()],
+ [WidgetButtonState.disabled, new DisabledButtonStateCssGenerator()]
+ ]
+);
+
+const widgetButtonCssSelector = '.mat-mdc-button.mat-mdc-button-base.tb-widget-button';
+
+export const generateWidgetButtonAppearanceCss = (appearance: WidgetButtonAppearance): string => {
+ let statesCss = '';
+ for (const state of widgetButtonStates) {
+ const generator = buttonStateCssGeneratorsMap.get(state);
+ statesCss += `\n${generator.generateStateCss(appearance)}`;
+ }
+ return `${widgetButtonCssSelector} {\n`+
+ `${statesCss}\n`+
+ `}`;
+};
+
+const darkenColor = (inputColor: string, amount: number): string => {
+ const input = tinycolor(inputColor);
+ const brightness = input.getBrightness() / 255;
+ let ratio: number;
+ if (brightness >= 0.4 && brightness <= 0.5) {
+ ratio = brightness + 0.2;
+ } else {
+ ratio = Math.max(0.1, Math.log10(brightness * 8));
+ }
+ return input.darken(ratio * amount).toRgbString();
+};
diff --git a/ui-ngx/src/app/shared/components/color-input.component.ts b/ui-ngx/src/app/shared/components/color-input.component.ts
index 171e8bf7d8..ad772ba861 100644
--- a/ui-ngx/src/app/shared/components/color-input.component.ts
+++ b/ui-ngx/src/app/shared/components/color-input.component.ts
@@ -189,7 +189,7 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
this.popoverService.hidePopover(trigger);
} else {
const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer,
- this.viewContainerRef, ColorPickerPanelComponent, 'left', true, null,
+ this.viewContainerRef, ColorPickerPanelComponent, ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null,
{
color: this.colorFormGroup.get('color').value,
colorClearButton: this.colorClearButton
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss
index c55222a7d2..9aca580994 100644
--- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss
+++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss
@@ -18,9 +18,11 @@
display: flex;
flex-direction: column;
gap: 32px;
+ overflow: auto;
.saturation-component {
height: 238px;
+ min-height: 80px;
border-radius: 8px;
}
diff --git a/ui-ngx/src/app/shared/models/action-widget-settings.models.ts b/ui-ngx/src/app/shared/models/action-widget-settings.models.ts
index d1442c09c5..d7f4710f8d 100644
--- a/ui-ngx/src/app/shared/models/action-widget-settings.models.ts
+++ b/ui-ngx/src/app/shared/models/action-widget-settings.models.ts
@@ -15,6 +15,7 @@
///
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
+import { widgetType } from '@shared/models/widget.models';
export enum GetValueAction {
DO_NOTHING = 'DO_NOTHING',
@@ -25,6 +26,14 @@ export enum GetValueAction {
export const getValueActions = Object.keys(GetValueAction) as GetValueAction[];
+export const getValueActionsByWidgetType = (type: widgetType): GetValueAction[] => {
+ if (type !== widgetType.rpc) {
+ return getValueActions.filter(action => action !== GetValueAction.EXECUTE_RPC);
+ } else {
+ return getValueActions;
+ }
+};
+
export const getValueActionTranslations = new Map(
[
[GetValueAction.DO_NOTHING, 'widgets.value-action.do-nothing'],
@@ -45,11 +54,7 @@ export interface TelemetryValueSettings {
key: string;
}
-export interface GetTelemetryValueSettings extends TelemetryValueSettings {
- subscribeForUpdates: boolean;
-}
-
-export interface GetAttributeValueSettings extends GetTelemetryValueSettings {
+export interface GetAttributeValueSettings extends TelemetryValueSettings {
scope: AttributeScope | null;
}
@@ -75,9 +80,9 @@ export interface ValueActionSettings {
export interface GetValueSettings extends ValueActionSettings {
action: GetValueAction;
defaultValue: V;
- executeRpc: RpcSettings;
+ executeRpc?: RpcSettings;
getAttribute: GetAttributeValueSettings;
- getTimeSeries: GetTelemetryValueSettings;
+ getTimeSeries: TelemetryValueSettings;
dataToValue: DataToValueSettings;
}
@@ -89,6 +94,14 @@ export enum SetValueAction {
export const setValueActions = Object.keys(SetValueAction) as SetValueAction[];
+export const setValueActionsByWidgetType = (type: widgetType): SetValueAction[] => {
+ if (type !== widgetType.rpc) {
+ return setValueActions.filter(action => action !== SetValueAction.EXECUTE_RPC);
+ } else {
+ return setValueActions;
+ }
+};
+
export const setValueActionTranslations = new Map(
[
[SetValueAction.EXECUTE_RPC, 'widgets.value-action.execute-rpc'],
@@ -116,13 +129,3 @@ export interface SetValueSettings extends ValueActionSettings {
putTimeSeries: TelemetryValueSettings;
valueToData: ValueToDataSettings;
}
-
-/*export interface RpcStateBehaviourSettings {
- initialState: RpcInitialStateSettings;
- updateStateByValue: (value: V) => RpcUpdateStateSettings;
-}
-
-export interface RpcStateWidgetSettings {
- initialState: RpcInitialStateSettings;
- background: BackgroundSettings;
-}*/
diff --git a/ui-ngx/src/app/shared/models/widget-settings.models.ts b/ui-ngx/src/app/shared/models/widget-settings.models.ts
index cf72e20025..5bd9e26f13 100644
--- a/ui-ngx/src/app/shared/models/widget-settings.models.ts
+++ b/ui-ngx/src/app/shared/models/widget-settings.models.ts
@@ -15,7 +15,14 @@
///
import { isDefinedAndNotNull, isNumber, isNumeric, isUndefinedOrNull, parseFunction } from '@core/utils';
-import { DataEntry, DataKey, Datasource, DatasourceData } from '@shared/models/widget.models';
+import {
+ DataEntry,
+ DataKey,
+ Datasource,
+ DatasourceData,
+ DatasourceType,
+ TargetDevice, TargetDeviceType
+} from '@shared/models/widget.models';
import { Injector } from '@angular/core';
import { DatePipe } from '@angular/common';
import { DateAgoPipe } from '@shared/pipe/date-ago.pipe';
@@ -600,6 +607,24 @@ export const updateDataKeyByLabel = (datasources: Datasource[], dataKey: DataKey
}
};
+export const getTargetDeviceFromDatasources = (datasources?: Datasource[]): TargetDevice => {
+ if (datasources && datasources.length) {
+ const datasource = datasources[0];
+ if (datasource?.type === DatasourceType.device) {
+ return {
+ type: TargetDeviceType.device,
+ deviceId: datasource?.deviceId
+ };
+ } else if (datasource?.type === DatasourceType.entity) {
+ return {
+ type: TargetDeviceType.entity,
+ entityAliasId: datasource?.entityAliasId
+ };
+ }
+ }
+ return null;
+};
+
export const getAlarmFilterConfig = (datasources?: Datasource[]): AlarmFilterConfig => {
if (datasources && datasources.length) {
const config = datasources[0].alarmFilterConfig;
diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts
index 3a13f51b24..625d957cff 100644
--- a/ui-ngx/src/app/shared/models/widget.models.ts
+++ b/ui-ngx/src/app/shared/models/widget.models.ts
@@ -180,6 +180,7 @@ export interface WidgetTypeParameters {
previewWidth?: string;
previewHeight?: string;
embedTitlePanel?: boolean;
+ overflowVisible?: boolean;
hideDataSettings?: boolean;
defaultDataKeysFunction?: (configComponent: any, configData: any) => DataKey[];
defaultLatestDataKeysFunction?: (configComponent: any, configData: any) => DataKey[];
@@ -410,6 +411,23 @@ export interface Datasource {
[key: string]: any;
}
+export const datasourceValid = (datasource: Datasource): boolean => {
+ const type: DatasourceType = datasource?.type;
+ if (type) {
+ switch (type) {
+ case DatasourceType.function:
+ case DatasourceType.alarmCount:
+ return true;
+ case DatasourceType.device:
+ return !!datasource.deviceId;
+ case DatasourceType.entity:
+ case DatasourceType.entityCount:
+ return !!datasource.entityAliasId;
+ }
+ }
+ return false;
+};
+
export enum TargetDeviceType {
device = 'device',
entity = 'entity'
@@ -675,6 +693,14 @@ export const actionDescriptorToAction = (descriptor: WidgetActionDescriptor): Wi
return result;
};
+export const defaultWidgetAction = (setEntityId = true): WidgetAction => ({
+ type: WidgetActionType.updateDashboardState,
+ targetDashboardStateId: null,
+ openRightLayout: false,
+ setEntityId,
+ stateEntityParamName: null
+ });
+
export interface WidgetComparisonSettings {
comparisonEnabled?: boolean;
timeForComparison?: moment_.unitOfTime.DurationConstructor;
diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts
index 0abf2167f6..71f121ab17 100644
--- a/ui-ngx/src/app/shared/shared.module.ts
+++ b/ui-ngx/src/app/shared/shared.module.ts
@@ -217,6 +217,7 @@ import { MultipleGalleryImageInputComponent } from '@shared/components/image/mul
import { EmbedImageDialogComponent } from '@shared/components/image/embed-image-dialog.component';
import { ImageGalleryDialogComponent } from '@shared/components/image/image-gallery-dialog.component';
import { RuleChainSelectPanelComponent } from '@shared/components/rule-chain/rule-chain-select-panel.component';
+import { WidgetButtonComponent } from '@shared/components/button/widget-button.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@@ -414,7 +415,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GalleryImageInputComponent,
MultipleGalleryImageInputComponent,
EmbedImageDialogComponent,
- ImageGalleryDialogComponent
+ ImageGalleryDialogComponent,
+ WidgetButtonComponent
],
imports: [
CommonModule,
@@ -666,7 +668,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GalleryImageInputComponent,
MultipleGalleryImageInputComponent,
EmbedImageDialogComponent,
- ImageGalleryDialogComponent
+ ImageGalleryDialogComponent,
+ WidgetButtonComponent
]
})
export class SharedModule { }
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 e18d21f609..ae3bc28c31 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -5182,6 +5182,42 @@
"invalid-widget-file-error": "Unable to import widget: Invalid widget data structure."
},
"widgets": {
+ "action-button": {
+ "behavior": "Behavior",
+ "on-click": "On click",
+ "on-click-hint": "Action performed when the button is clicked."
+ },
+ "button": {
+ "layout": "Layout",
+ "outlined": "Outlined",
+ "filled": "Filled",
+ "underlined": "Underlined",
+ "basic": "Basic",
+ "auto-scale": "Auto scale",
+ "label": "Label",
+ "icon": "Icon",
+ "color-palette": "Color palette",
+ "main": "Main",
+ "background": "Background",
+ "custom-styles": "Custom styles",
+ "clear-style": "Clear style",
+ "shadow": "Shadow",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "preview": "Preview",
+ "copy-style-from": "Copy style from"
+ },
+ "button-state": {
+ "activated-state": "Activated state",
+ "activated-state-hint": "Condition under which the button is active.",
+ "disabled-state": "Disabled state",
+ "disabled-state-hint": "Condition under which the button is disabled.",
+ "enabled": "Enabled",
+ "hovered": "Hovered",
+ "pressed": "Pressed",
+ "activated": "Activated",
+ "disabled": "Disabled"
+ },
"background": {
"background": "Background",
"background-settings": "Background settings",
@@ -6483,15 +6519,20 @@
"source-entity-alias": "Source entity alias",
"source-entity-attribute": "Source entity attribute"
},
- "value-action": {
+ "rpc-state": {
"initial-state": "Initial state",
"initial-state-hint": "Action to get the initial value of the component.",
+ "disabled-state": "Disabled state",
+ "disabled-state-hint": "Condition under which the component is disabled.",
"turn-on": "Turn 'On'",
"turn-on-hint": "Action performed to turn ON the component.",
"turn-off": "Turn 'Off'",
"turn-off-hint": "Action performed to turn OFF the component.",
"on": "On",
"off": "Off",
+ "disabled": "Disabled"
+ },
+ "value-action": {
"do-nothing": "Do nothing",
"execute-rpc": "Execute RPC",
"get-attribute": "Get attribute",
@@ -6523,14 +6564,12 @@
"attribute-key-required": "Attribute key is required.",
"time-series-key": "Time-series key",
"time-series-key-required": "Time-series key is required.",
- "subscribe-for-updates": "Subscribe for updates",
- "subscribe-for-updates-hint": "Subscribe for updates",
"action-result-converter": "Action result converter",
"converter-none": "None",
"converter-function": "Function",
"converter-constant": "Constant",
"parse-value-function": "Parse value function",
- "on-when-result-is": "'On' when result is",
+ "state-when-result-is": "'{{state}}' when result is",
"parameters": "Parameters",
"convert-value-function": "Convert value function",
"error": {
@@ -6785,7 +6824,8 @@
"element-click": "On HTML element click",
"pie-slice-click": "On slice click",
"row-double-click": "On row double click",
- "card-click": "On card click"
+ "card-click": "On card click",
+ "click": "On click"
}
},
"paginator" : {
diff --git a/ui-ngx/src/assets/widget/button/basic.svg b/ui-ngx/src/assets/widget/button/basic.svg
new file mode 100644
index 0000000000..6b005e345e
--- /dev/null
+++ b/ui-ngx/src/assets/widget/button/basic.svg
@@ -0,0 +1,17 @@
+
diff --git a/ui-ngx/src/assets/widget/button/filled.svg b/ui-ngx/src/assets/widget/button/filled.svg
new file mode 100644
index 0000000000..c17871583a
--- /dev/null
+++ b/ui-ngx/src/assets/widget/button/filled.svg
@@ -0,0 +1,19 @@
+
diff --git a/ui-ngx/src/assets/widget/button/outlined.svg b/ui-ngx/src/assets/widget/button/outlined.svg
new file mode 100644
index 0000000000..63b2232060
--- /dev/null
+++ b/ui-ngx/src/assets/widget/button/outlined.svg
@@ -0,0 +1,20 @@
+
diff --git a/ui-ngx/src/assets/widget/button/underlined.svg b/ui-ngx/src/assets/widget/button/underlined.svg
new file mode 100644
index 0000000000..6638662a21
--- /dev/null
+++ b/ui-ngx/src/assets/widget/button/underlined.svg
@@ -0,0 +1,23 @@
+
diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss
index d6fceb20f7..635d330572 100644
--- a/ui-ngx/src/form.scss
+++ b/ui-ngx/src/form.scss
@@ -607,6 +607,14 @@
}
}
+ button.mat-mdc-button-base.tb-nowrap {
+ .mdc-button__label {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
.mat-mdc-chip-listbox.center-stretch {
.mat-mdc-standard-chip {
flex: 1;