-
-
-
-
-
-
- {{ 'widget-config.drop-shadow' | translate }}
-
-
- {{ 'widget-config.enable-fullscreen' | translate }}
-
-
-
-
- widget-config.advanced-settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ 'widget-config.display-legend' | translate }}
-
-
-
- widget-config.advanced-settings
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ 'widget-config.mobile-hide' | translate }}
-
-
- {{ 'widget-config.desktop-hide' | translate }}
-
-
-
- widget-config.advanced-settings
-
-
-
-
-
- widget-config.order
-
-
-
- widget-config.height
-
-
-
-
-
-
+
-
-
-
-
-
+
+
+
+ {{ 'widget-config.mobile-hide' | translate }}
+
+
+
+
+ {{ 'widget-config.desktop-hide' | translate }}
+
+
+
+
+
+
+
+
+
+
{{basicModeDirectiveError}}
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
index 2ae2eaa211..91b3368ad0 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
@@ -13,118 +13,46 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+@import "../../../../../theme";
@import '../../../../../scss/constants';
.tb-widget-config {
- .tb-panel-hint {
- font-size: 12px;
- color: #808080;
- }
-}
-
-.tb-datasource-list-item {
- &.mat-mdc-list-item {
- height: auto;
- min-height: 68px;
- display: block;
- padding: 0;
- }
- &.tb-draggable {
- &.cdk-drag-preview {
- background: #fff;
- }
- }
-}
-
-.tb-datasource-params {
- position: relative;
- padding: 0 0 0 10px;
- margin: 5px;
- tb-error.tb-datasource-error {
- position: absolute;
- bottom: 4px;
- left: 8px;
- }
- .tb-datasource-section {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ .tb-widget-config-header {
+ padding: 24px 24px 8px;
+ height: 56px;
display: flex;
- flex-direction: column;
- align-items: stretch;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ }
+ .tb-widget-config-content {
flex: 1;
- padding-top: 20px;
- @media #{$mat-gt-sm} {
- flex-direction: row;
- align-items: center;
- justify-content: flex-start;
+ overflow: auto;
+ & > div {
+ padding: 16px;
}
- }
- .tb-datasource-type {
- min-width: 160px;
- @media #{$mat-gt-sm} {
- max-width: 160px;
+ .mat-content {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
}
}
- .tb-datasource {
- @media #{$mat-gt-sm} {
- padding-left: 8px;
- width: 208px;
- max-width: 208px;
- }
- }
- .tb-data-keys {
- @media #{$mat-gt-sm} {
- padding-left: 8px;
- }
- }
-}
-
-:host {
- .tb-widget-config {
- .tb-advanced-widget-config {
- height: 100%;
- }
- .tb-datasources {
-
- .handle {
- cursor: move;
- }
-
- .mat-mdc-list {
- min-height: 68px;
- padding-left: 0;
- }
- }
- .fields-group {
- padding: 0 16px 8px;
- margin-bottom: 10px;
- border: 1px groove rgba(0, 0, 0, .25);
- border-radius: 4px;
- legend {
- color: rgba(0, 0, 0, .7);
- width: fit-content;
- }
- }
- .fields-group-slider {
- padding: 0;
- legend {
- margin-left: 16px;
- }
- .tb-settings {
- padding: 0 16px 8px;
- }
- }
- .tb-widget-style {
- margin-top: 16px;
- }
+ .tb-basic-mode-directive-error {
+ font-size: 13px;
+ font-weight: 400;
+ color: rgb(221, 44, 0);
}
}
:host ::ng-deep {
.tb-widget-config {
- tb-alarm-filter-config {
- .mdc-button {
- width: 100%;
- height: 100%;
- justify-content: flex-start;
+ .tb-widget-config-header {
+ .mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
+ padding: 0 20px;
}
}
.mat-mdc-tab-body-wrapper {
@@ -134,53 +62,5 @@
right: 0;
bottom: 0;
}
- .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-content {
- > .mat-expansion-panel-body {
- padding: 0;
- }
- }
- .tb-json-object-panel, .tb-css-content-panel {
- margin: 0 0 8px;
- }
- }
- &.tb-datasources {
- &.mat-expanded {
- overflow: visible;
- }
- .mat-expansion-panel-body{
- padding: 0 12px 16px;
- }
- }
- .mat-expansion-panel-content {
- font: inherit;
- }
- }
- .mat-slide {
- margin: 8px 0;
- }
- .slide-block {
- display: block;
- &:not(:last-child) {
- margin-bottom: 8px;
- }
- }
}
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
index 1514c1b929..999e7e55ff 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
@@ -14,29 +14,38 @@
/// limitations under the License.
///
-import { Component, forwardRef, Input, OnInit } from '@angular/core';
+import {
+ ChangeDetectorRef,
+ Component,
+ ComponentFactoryResolver,
+ ComponentRef,
+ forwardRef,
+ Input,
+ OnChanges,
+ OnDestroy,
+ OnInit,
+ SimpleChanges,
+ ViewChild,
+ ViewContainerRef
+} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
DataKey,
- Datasource,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation,
- DatasourceType,
- datasourceTypeTranslationMap,
- defaultLegendConfig,
GroupInfo,
JsonSchema,
JsonSettingsSchema,
Widget,
+ WidgetConfigMode,
widgetType
} from '@shared/models/widget.models';
import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
- UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
@@ -53,7 +62,10 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models';
import { Observable, of, Subscription } from 'rxjs';
-import { WidgetConfigCallbacks } from '@home/components/widget/widget-config.component.models';
+import {
+ IBasicWidgetConfigComponent,
+ WidgetConfigCallbacks
+} from '@home/components/widget/config/widget-config.component.models';
import {
EntityAliasDialogComponent,
EntityAliasDialogData
@@ -62,12 +74,15 @@ import { catchError, mergeMap, tap } from 'rxjs/operators';
import { MatDialog } from '@angular/material/dialog';
import { EntityService } from '@core/http/entity.service';
import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
-import { WidgetActionsData } from './action/manage-widget-actions.component.models';
import { Dashboard } from '@shared/models/dashboard.models';
import { entityFields } from '@shared/models/entity.models';
-import { Filter } from '@shared/models/query/query.models';
+import { Filter, singleEntityFilterFromDeviceId } from '@shared/models/query/query.models';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
-import { CdkDragDrop } from '@angular/cdk/drag-drop';
+import { ToggleHeaderOption } from '@shared/components/toggle-header.component';
+import { coerceBoolean } from '@shared/decorators/coercion';
+import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module';
+import { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component';
+import Timeout = NodeJS.Timeout;
const emptySettingsSchema: JsonSchema = {
type: 'object',
@@ -95,10 +110,14 @@ const defaultSettingsForm = [
}
]
})
-export class WidgetConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator {
+export class WidgetConfigComponent extends PageComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator, OnChanges {
+
+ @ViewChild('basicModeContainer', {read: ViewContainerRef, static: false}) basicModeContainer: ViewContainerRef;
widgetTypes = widgetType;
+ widgetConfigModes = WidgetConfigMode;
+
entityTypes = EntityType;
@Input()
@@ -116,65 +135,80 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
@Input()
functionsOnly: boolean;
+ @Input()
+ @coerceBoolean()
+ hideHeader = false;
+
+ @Input()
+ @coerceBoolean()
+ hideToggleHeader = false;
+
+ @Input()
+ @coerceBoolean()
+ isAdd = false;
+
@Input() disabled: boolean;
- widgetType: widgetType;
+ @Input()
+ widgetConfigMode = WidgetConfigMode.advanced;
- datasourceType = DatasourceType;
- datasourceTypes: Array
= [];
- datasourceTypesTranslations = datasourceTypeTranslationMap;
+ widgetType: widgetType;
widgetConfigCallbacks: WidgetConfigCallbacks = {
createEntityAlias: this.createEntityAlias.bind(this),
createFilter: this.createFilter.bind(this),
generateDataKey: this.generateDataKey.bind(this),
+ fetchEntityKeysForDevice: this.fetchEntityKeysForDevice.bind(this),
fetchEntityKeys: this.fetchEntityKeys.bind(this),
fetchDashboardStates: this.fetchDashboardStates.bind(this)
};
widgetEditMode = this.utils.widgetEditMode;
- selectedTab: number;
+ basicModeDirectiveError: string;
modelValue: WidgetConfigComponentData;
- showLegendFieldset = true;
-
private propagateChange = null;
+ headerOptions: ToggleHeaderOption[] = [];
+ selectedOption: string;
+
public dataSettings: UntypedFormGroup;
public targetDeviceSettings: UntypedFormGroup;
- public alarmSourceSettings: UntypedFormGroup;
public widgetSettings: UntypedFormGroup;
public layoutSettings: UntypedFormGroup;
public advancedSettings: UntypedFormGroup;
public actionsSettings: UntypedFormGroup;
- public openExtensionPanel = true;
- public timeseriesKeyError = false;
- public datasourceError: string[] = [];
+ private createBasicModeComponentTimeout: Timeout;
+ private basicModeComponentRef: ComponentRef;
+ private basicModeComponent: IBasicWidgetConfigComponent;
+ private basicModeComponentChangeSubscription: Subscription;
private dataSettingsChangesSubscription: Subscription;
private targetDeviceSettingsSubscription: Subscription;
- private alarmSourceSettingsSubscription: Subscription;
private widgetSettingsSubscription: Subscription;
private layoutSettingsSubscription: Subscription;
private advancedSettingsSubscription: Subscription;
private actionsSettingsSubscription: Subscription;
+ private defaultConfigFormsType: widgetType;
+
constructor(protected store: Store,
private utils: UtilsService,
private entityService: EntityService,
private dialog: MatDialog,
- private translate: TranslateService,
- private fb: UntypedFormBuilder) {
+ public translate: TranslateService,
+ private cfr: ComponentFactoryResolver,
+ private fb: UntypedFormBuilder,
+ private cd: ChangeDetectorRef) {
super(store);
}
ngOnInit(): void {
this.dataSettings = this.fb.group({});
this.targetDeviceSettings = this.fb.group({});
- this.alarmSourceSettings = this.fb.group({});
this.advancedSettings = this.fb.group({});
this.widgetSettings = this.fb.group({
title: [null, []],
@@ -196,41 +230,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
pageSize: [1024, [Validators.min(1), Validators.pattern(/^\d*$/)]],
units: [null, []],
decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]],
- noDataDisplayMessage: [null, []],
- showLegend: [null, []],
- legendConfig: [null, []]
- });
- this.widgetSettings.get('showTitle').valueChanges.subscribe((value: boolean) => {
- if (value) {
- this.widgetSettings.get('titleStyle').enable({emitEvent: false});
- this.widgetSettings.get('titleTooltip').enable({emitEvent: false});
- this.widgetSettings.get('showTitleIcon').enable({emitEvent: false});
- } else {
- this.widgetSettings.get('titleStyle').disable({emitEvent: false});
- this.widgetSettings.get('titleTooltip').disable({emitEvent: false});
- this.widgetSettings.get('showTitleIcon').patchValue(false);
- this.widgetSettings.get('showTitleIcon').disable({emitEvent: false});
- }
+ noDataDisplayMessage: [null, []]
});
- this.widgetSettings.get('showTitleIcon').valueChanges.subscribe((value: boolean) => {
- if (value) {
- this.widgetSettings.get('titleIcon').enable({emitEvent: false});
- this.widgetSettings.get('iconColor').enable({emitEvent: false});
- this.widgetSettings.get('iconSize').enable({emitEvent: false});
- } else {
- this.widgetSettings.get('titleIcon').disable({emitEvent: false});
- this.widgetSettings.get('iconColor').disable({emitEvent: false});
- this.widgetSettings.get('iconSize').disable({emitEvent: false});
- }
+ this.widgetSettings.get('showTitle').valueChanges.subscribe(() => {
+ this.updateWidgetSettingsEnabledState();
});
- this.widgetSettings.get('showLegend').valueChanges.subscribe((value: boolean) => {
- if (value) {
- this.widgetSettings.get('legendConfig').enable({emitEvent: false});
- } else {
- this.widgetSettings.get('legendConfig').disable({emitEvent: false});
- }
+ this.widgetSettings.get('showTitleIcon').valueChanges.subscribe(() => {
+ this.updateWidgetSettingsEnabledState();
});
+
this.layoutSettings = this.fb.group({
mobileOrder: [null, [Validators.pattern(/^-?[0-9]+$/)]],
mobileHeight: [null, [Validators.min(1), Validators.pattern(/^\d*$/)]],
@@ -238,11 +247,25 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
desktopHide: [false]
});
this.actionsSettings = this.fb.group({
- actionsData: [null, []]
+ actions: [null, []]
});
}
+ ngOnChanges(changes: SimpleChanges): void {
+ for (const propName of Object.keys(changes)) {
+ const change = changes[propName];
+ if (!change.firstChange && change.currentValue !== change.previousValue) {
+ if (propName === 'widgetConfigMode') {
+ if (this.hasBasicModeDirective) {
+ this.setupConfig();
+ }
+ }
+ }
+ }
+ }
+
ngOnDestroy(): void {
+ this.destroyBasicModeComponent();
this.removeChangeSubscriptions();
}
@@ -255,10 +278,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.targetDeviceSettingsSubscription.unsubscribe();
this.targetDeviceSettingsSubscription = null;
}
- if (this.alarmSourceSettingsSubscription) {
- this.alarmSourceSettingsSubscription.unsubscribe();
- this.alarmSourceSettingsSubscription = null;
- }
if (this.widgetSettingsSubscription) {
this.widgetSettingsSubscription.unsubscribe();
this.widgetSettingsSubscription = null;
@@ -284,9 +303,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe(
() => this.updateTargetDeviceSettings()
);
- this.alarmSourceSettingsSubscription = this.alarmSourceSettings.valueChanges.subscribe(
- () => this.updateAlarmSourceSettings()
- );
this.widgetSettingsSubscription = this.widgetSettings.valueChanges.subscribe(
() => this.updateWidgetSettings()
);
@@ -301,34 +317,57 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
);
}
- private buildForms() {
- if (this.functionsOnly) {
- this.datasourceTypes = [DatasourceType.function];
- } else {
- this.datasourceTypes = [DatasourceType.function, DatasourceType.entity];
- if (this.widgetType === widgetType.latest) {
- this.datasourceTypes.push(DatasourceType.entityCount);
- this.datasourceTypes.push(DatasourceType.alarmCount);
+ private buildHeader() {
+ this.headerOptions.length = 0;
+ if (this.widgetType !== widgetType.static) {
+ this.headerOptions.push(
+ {
+ name: this.translate.instant('widget-config.data'),
+ value: 'data'
+ }
+ );
+ }
+ if (this.displayAppearance) {
+ this.headerOptions.push(
+ {
+ name: this.translate.instant('widget-config.appearance'),
+ value: 'appearance'
+ }
+ );
+ }
+ this.headerOptions.push(
+ {
+ name: this.translate.instant('widget-config.widget-card'),
+ value: 'card'
+ }
+ );
+ this.headerOptions.push(
+ {
+ name: this.translate.instant('widget-config.actions'),
+ value: 'actions'
+ }
+ );
+ this.headerOptions.push(
+ {
+ name: this.translate.instant('widget-config.mobile'),
+ value: 'mobile'
}
+ );
+ if (!this.selectedOption || !this.headerOptions.find(o => o.value === this.selectedOption)) {
+ this.selectedOption = this.headerOptions[0].value;
}
+ }
+ private buildForms() {
this.dataSettings = this.fb.group({});
this.targetDeviceSettings = this.fb.group({});
- this.alarmSourceSettings = this.fb.group({});
this.advancedSettings = this.fb.group({});
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) {
- this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(true));
- this.dataSettings.addControl('displayTimewindow', this.fb.control({value: true, disabled: true}));
- this.dataSettings.addControl('timewindow', this.fb.control({value: null, disabled: true}));
- this.dataSettings.get('useDashboardTimewindow').valueChanges.subscribe((value: boolean) => {
- if (value) {
- this.dataSettings.get('displayTimewindow').disable({emitEvent: false});
- this.dataSettings.get('timewindow').disable({emitEvent: false});
- } else {
- this.dataSettings.get('displayTimewindow').enable({emitEvent: false});
- this.dataSettings.get('timewindow').enable({emitEvent: false});
- }
- });
+ this.dataSettings.addControl('timewindowConfig', this.fb.control({
+ useDashboardTimewindow: true,
+ displayTimewindow: true,
+ timewindow: null
+ }));
if (this.widgetType === widgetType.alarm) {
this.dataSettings.addControl('alarmFilterConfig', this.fb.control(null));
}
@@ -337,24 +376,19 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
if (this.widgetType !== widgetType.rpc &&
this.widgetType !== widgetType.alarm &&
this.widgetType !== widgetType.static) {
- this.dataSettings.addControl('datasources',
- this.fb.array([]));
+ this.dataSettings.addControl('datasources', this.fb.control(null));
} else if (this.widgetType === widgetType.rpc) {
this.targetDeviceSettings.addControl('targetDeviceAliasId',
this.fb.control(null,
this.widgetEditMode ? [] : [Validators.required]));
} else if (this.widgetType === widgetType.alarm) {
- this.alarmSourceSettings = this.buildDatasourceForm();
+ this.dataSettings.addControl('alarmSource', this.fb.control(null));
}
}
this.advancedSettings.addControl('settings',
this.fb.control(null, []));
}
- datasourcesFormArray(): UntypedFormArray {
- return this.dataSettings.get('datasources') as UntypedFormArray;
- }
-
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
@@ -368,213 +402,200 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
writeValue(value: WidgetConfigComponentData): void {
this.modelValue = value;
- this.removeChangeSubscriptions();
+ this.widgetType = this.modelValue?.widgetType;
+ this.setupConfig(this.isAdd);
+ }
+
+ private setupConfig(isAdd = false) {
if (this.modelValue) {
- if (this.widgetType !== this.modelValue.widgetType) {
- this.widgetType = this.modelValue.widgetType;
- this.showLegendFieldset = (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.latest);
- this.buildForms();
+ this.destroyBasicModeComponent();
+ this.removeChangeSubscriptions();
+ if (this.hasBasicModeDirective && this.widgetConfigMode === WidgetConfigMode.basic) {
+ this.setupBasicModeConfig(isAdd);
+ } else {
+ this.setupDefaultConfig();
}
- const config = this.modelValue.config;
- const layout = this.modelValue.layout;
- if (config) {
- this.selectedTab = 0;
- const displayWidgetTitle = isDefined(config.showTitle) ? config.showTitle : false;
- this.widgetSettings.patchValue({
- title: config.title,
- showTitleIcon: isDefined(config.showTitleIcon) && displayWidgetTitle ? config.showTitleIcon : false,
- titleIcon: isDefined(config.titleIcon) ? config.titleIcon : '',
- iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)',
- iconSize: isDefined(config.iconSize) ? config.iconSize : '24px',
- titleTooltip: isDefined(config.titleTooltip) ? config.titleTooltip : '',
- showTitle: displayWidgetTitle,
- dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true,
- enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true,
- backgroundColor: config.backgroundColor,
- color: config.color,
- padding: config.padding,
- margin: config.margin,
- widgetStyle: isDefined(config.widgetStyle) ? config.widgetStyle : {},
- widgetCss: isDefined(config.widgetCss) ? config.widgetCss : '',
- titleStyle: isDefined(config.titleStyle) ? config.titleStyle : {
- fontSize: '16px',
- fontWeight: 400
- },
- pageSize: isDefined(config.pageSize) ? config.pageSize : 1024,
- units: config.units,
- decimals: config.decimals,
- noDataDisplayMessage: isDefined(config.noDataDisplayMessage) ? config.noDataDisplayMessage : '',
- showLegend: isDefined(config.showLegend) ? config.showLegend :
- this.widgetType === widgetType.timeseries,
- legendConfig: config.legendConfig || defaultLegendConfig(this.widgetType)
- },
- {emitEvent: false}
- );
- const showTitle: boolean = this.widgetSettings.get('showTitle').value;
- if (showTitle) {
- this.widgetSettings.get('titleTooltip').enable({emitEvent: false});
- this.widgetSettings.get('titleStyle').enable({emitEvent: false});
- this.widgetSettings.get('showTitleIcon').enable({emitEvent: false});
- } else {
- this.widgetSettings.get('titleTooltip').disable({emitEvent: false});
- this.widgetSettings.get('titleStyle').disable({emitEvent: false});
- this.widgetSettings.get('showTitleIcon').disable({emitEvent: false});
- }
- const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value;
- if (showTitleIcon) {
- this.widgetSettings.get('titleIcon').enable({emitEvent: false});
- this.widgetSettings.get('iconColor').enable({emitEvent: false});
- this.widgetSettings.get('iconSize').enable({emitEvent: false});
- } else {
- this.widgetSettings.get('titleIcon').disable({emitEvent: false});
- this.widgetSettings.get('iconColor').disable({emitEvent: false});
- this.widgetSettings.get('iconSize').disable({emitEvent: false});
- }
- const showLegend: boolean = this.widgetSettings.get('showLegend').value;
- if (showLegend) {
- this.widgetSettings.get('legendConfig').enable({emitEvent: false});
- } else {
- this.widgetSettings.get('legendConfig').disable({emitEvent: false});
- }
- const actionsData: WidgetActionsData = {
- actionsMap: config.actions || {},
- actionSources: this.modelValue.actionSources || {}
- };
- this.actionsSettings.patchValue(
- {
- actionsData
+ }
+ }
+
+ private setupBasicModeConfig(isAdd = false) {
+ const componentType = basicWidgetConfigComponentsMap[this.modelValue.basicModeDirective];
+ if (!componentType) {
+ this.basicModeDirectiveError = this.translate.instant('widget-config.settings-component-not-found',
+ {selector: this.modelValue.basicModeDirective});
+ } else {
+ const factory = this.cfr.resolveComponentFactory(componentType);
+ this.createBasicModeComponentTimeout = setTimeout(() => {
+ this.createBasicModeComponentTimeout = null;
+ this.basicModeComponentRef = this.basicModeContainer.createComponent(factory);
+ this.basicModeComponent = this.basicModeComponentRef.instance;
+ this.basicModeComponent.isAdd = isAdd;
+ this.basicModeComponent.widgetConfig = this.modelValue;
+ this.basicModeComponentChangeSubscription = this.basicModeComponent.widgetConfigChanged.subscribe((data) => {
+ this.modelValue = data;
+ this.propagateChange(this.modelValue);
+ });
+ this.cd.markForCheck();
+ }, 0);
+ }
+ }
+
+ private destroyBasicModeComponent() {
+ this.basicModeDirectiveError = null;
+ if (this.basicModeComponentChangeSubscription) {
+ this.basicModeComponentChangeSubscription.unsubscribe();
+ this.basicModeComponentChangeSubscription = null;
+ }
+ if (this.createBasicModeComponentTimeout) {
+ clearTimeout(this.createBasicModeComponentTimeout);
+ this.createBasicModeComponentTimeout = null;
+ }
+ if (this.basicModeComponentRef) {
+ this.basicModeComponentRef.destroy();
+ this.basicModeComponentRef = null;
+ this.basicModeComponent = null;
+ }
+ if (this.basicModeContainer) {
+ this.basicModeContainer.clear();
+ }
+ }
+
+ private setupDefaultConfig() {
+ if (this.defaultConfigFormsType !== this.widgetType) {
+ this.defaultConfigFormsType = this.widgetType;
+ this.buildForms();
+ }
+ this.buildHeader();
+ const config = this.modelValue.config;
+ const layout = this.modelValue.layout;
+ if (config) {
+ const displayWidgetTitle = isDefined(config.showTitle) ? config.showTitle : false;
+ this.widgetSettings.patchValue({
+ title: config.title,
+ showTitleIcon: isDefined(config.showTitleIcon) && displayWidgetTitle ? config.showTitleIcon : false,
+ titleIcon: isDefined(config.titleIcon) ? config.titleIcon : '',
+ iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)',
+ iconSize: isDefined(config.iconSize) ? config.iconSize : '24px',
+ titleTooltip: isDefined(config.titleTooltip) ? config.titleTooltip : '',
+ showTitle: displayWidgetTitle,
+ dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true,
+ enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true,
+ backgroundColor: config.backgroundColor,
+ color: config.color,
+ padding: config.padding,
+ margin: config.margin,
+ widgetStyle: isDefined(config.widgetStyle) ? config.widgetStyle : {},
+ widgetCss: isDefined(config.widgetCss) ? config.widgetCss : '',
+ titleStyle: isDefined(config.titleStyle) ? config.titleStyle : {
+ fontSize: '16px',
+ fontWeight: 400
},
- {emitEvent: false}
- );
- if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) {
- const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ?
- config.useDashboardTimewindow : true;
- this.dataSettings.patchValue(
- { useDashboardTimewindow }, {emitEvent: false}
- );
- if (useDashboardTimewindow) {
- this.dataSettings.get('displayTimewindow').disable({emitEvent: false});
- this.dataSettings.get('timewindow').disable({emitEvent: false});
- } else {
- this.dataSettings.get('displayTimewindow').enable({emitEvent: false});
- this.dataSettings.get('timewindow').enable({emitEvent: false});
- }
- this.dataSettings.patchValue(
- { displayTimewindow: isDefined(config.displayTimewindow) ?
- config.displayTimewindow : true }, {emitEvent: false}
- );
- this.dataSettings.patchValue(
- { timewindow: config.timewindow }, {emitEvent: false}
- );
- }
- if (this.modelValue.isDataEnabled) {
- if (this.widgetType !== widgetType.rpc &&
- this.widgetType !== widgetType.alarm &&
- this.widgetType !== widgetType.static) {
- const datasourcesFormArray = this.dataSettings.get('datasources') as UntypedFormArray;
- datasourcesFormArray.clear();
- if (config.datasources) {
- config.datasources.forEach((datasource) => {
- datasourcesFormArray.push(this.buildDatasourceForm(datasource));
- });
- }
- } else if (this.widgetType === widgetType.rpc) {
- let targetDeviceAliasId: string;
- if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) {
- const aliasId = config.targetDeviceAliasIds[0];
- const entityAliases = this.aliasController.getEntityAliases();
- if (entityAliases[aliasId]) {
- targetDeviceAliasId = entityAliases[aliasId].id;
- } else {
- targetDeviceAliasId = null;
- }
+ pageSize: isDefined(config.pageSize) ? config.pageSize : 1024,
+ units: config.units,
+ decimals: config.decimals,
+ noDataDisplayMessage: isDefined(config.noDataDisplayMessage) ? config.noDataDisplayMessage : ''
+ },
+ {emitEvent: false}
+ );
+ this.updateWidgetSettingsEnabledState();
+ this.actionsSettings.patchValue(
+ {
+ actions: config.actions || {}
+ },
+ {emitEvent: false}
+ );
+ if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) {
+ const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ?
+ config.useDashboardTimewindow : true;
+ this.dataSettings.get('timewindowConfig').patchValue({
+ useDashboardTimewindow,
+ displayTimewindow: isDefined(config.displayTimewindow) ?
+ config.displayTimewindow : true,
+ timewindow: config.timewindow
+ }, {emitEvent: false});
+ }
+ if (this.modelValue.isDataEnabled) {
+ if (this.widgetType !== widgetType.rpc &&
+ this.widgetType !== widgetType.alarm &&
+ this.widgetType !== widgetType.static) {
+ this.dataSettings.patchValue({ datasources: config.datasources},
+ {emitEvent: false});
+ } else if (this.widgetType === widgetType.rpc) {
+ let targetDeviceAliasId: string;
+ if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) {
+ const aliasId = config.targetDeviceAliasIds[0];
+ const entityAliases = this.aliasController.getEntityAliases();
+ if (entityAliases[aliasId]) {
+ targetDeviceAliasId = entityAliases[aliasId].id;
} else {
targetDeviceAliasId = null;
}
- this.targetDeviceSettings.patchValue({
- targetDeviceAliasId
- }, {emitEvent: false});
- } else if (this.widgetType === widgetType.alarm) {
- this.dataSettings.patchValue(
- { alarmFilterConfig: isDefined(config.alarmFilterConfig) ?
- config.alarmFilterConfig : { statusList: [AlarmSearchStatus.ACTIVE], searchPropagatedAlarms: true } }, {emitEvent: false}
- );
- this.alarmSourceSettings.patchValue(
- config.alarmSource, {emitEvent: false}
- );
- const alarmSourceType: DatasourceType = this.alarmSourceSettings.get('type').value;
- this.alarmSourceSettings.get('entityAliasId').setValidators(
- alarmSourceType === DatasourceType.entity ? [Validators.required] : []
- );
- this.alarmSourceSettings.get('entityAliasId').updateValueAndValidity({emitEvent: false});
+ } else {
+ targetDeviceAliasId = null;
}
- }
-
- this.updateSchemaForm(config.settings);
-
- if (layout) {
- this.layoutSettings.patchValue(
- {
- mobileOrder: layout.mobileOrder,
- mobileHeight: layout.mobileHeight,
- mobileHide: layout.mobileHide,
- desktopHide: layout.desktopHide
- },
- {emitEvent: false}
- );
- } else {
- this.layoutSettings.patchValue(
- {
- mobileOrder: null,
- mobileHeight: null,
- mobileHide: false,
- desktopHide: false
- },
- {emitEvent: false}
+ this.targetDeviceSettings.patchValue({
+ targetDeviceAliasId
+ }, {emitEvent: false});
+ } else if (this.widgetType === widgetType.alarm) {
+ this.dataSettings.patchValue(
+ { alarmFilterConfig: isDefined(config.alarmFilterConfig) ?
+ config.alarmFilterConfig :
+ { statusList: [AlarmSearchStatus.ACTIVE], searchPropagatedAlarms: true },
+ alarmSource: config.alarmSource }, {emitEvent: false}
);
}
}
- this.createChangeSubscriptions();
+
+ this.updateSchemaForm(config.settings);
+
+ if (layout) {
+ this.layoutSettings.patchValue(
+ {
+ mobileOrder: layout.mobileOrder,
+ mobileHeight: layout.mobileHeight,
+ mobileHide: layout.mobileHide,
+ desktopHide: layout.desktopHide
+ },
+ {emitEvent: false}
+ );
+ } else {
+ this.layoutSettings.patchValue(
+ {
+ mobileOrder: null,
+ mobileHeight: null,
+ mobileHide: false,
+ desktopHide: false
+ },
+ {emitEvent: false}
+ );
+ }
}
+ this.createChangeSubscriptions();
}
- public dataKeysOptional(type?: DatasourceType): boolean {
- if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) {
- return true;
+ private updateWidgetSettingsEnabledState() {
+ const showTitle: boolean = this.widgetSettings.get('showTitle').value;
+ const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value;
+ if (showTitle) {
+ this.widgetSettings.get('title').enable({emitEvent: false});
+ this.widgetSettings.get('titleTooltip').enable({emitEvent: false});
+ this.widgetSettings.get('titleStyle').enable({emitEvent: false});
+ this.widgetSettings.get('showTitleIcon').enable({emitEvent: false});
} else {
- return this.modelValue.typeParameters && this.modelValue.typeParameters.dataKeysOptional
- && type !== DatasourceType.entityCount && type !== DatasourceType.alarmCount;
+ this.widgetSettings.get('title').disable({emitEvent: false});
+ this.widgetSettings.get('titleTooltip').disable({emitEvent: false});
+ this.widgetSettings.get('titleStyle').disable({emitEvent: false});
+ this.widgetSettings.get('showTitleIcon').disable({emitEvent: false});
}
- }
-
- private buildDatasourceForm(datasource?: Datasource): UntypedFormGroup {
- const dataKeysRequired = !this.dataKeysOptional(datasource?.type);
- const datasourceFormGroup = this.fb.group(
- {
- type: [datasource ? datasource.type : null, [Validators.required]],
- name: [datasource ? datasource.name : null, []],
- entityAliasId: [datasource ? datasource.entityAliasId : null,
- datasource && (datasource.type === DatasourceType.entity ||
- datasource.type === DatasourceType.entityCount) ? [Validators.required] : []],
- filterId: [datasource ? datasource.filterId : null, []],
- dataKeys: [datasource ? datasource.dataKeys : null, dataKeysRequired ? [Validators.required] : []],
- alarmFilterConfig: [datasource && datasource.alarmFilterConfig ?
- datasource.alarmFilterConfig : { statusList: [AlarmSearchStatus.ACTIVE] }]
- }
- );
- if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) {
- datasourceFormGroup.addControl('latestDataKeys', this.fb.control(datasource ? datasource.latestDataKeys : null));
+ if (showTitle && showTitleIcon) {
+ this.widgetSettings.get('titleIcon').enable({emitEvent: false});
+ this.widgetSettings.get('iconColor').enable({emitEvent: false});
+ this.widgetSettings.get('iconSize').enable({emitEvent: false});
+ } else {
+ this.widgetSettings.get('titleIcon').disable({emitEvent: false});
+ this.widgetSettings.get('iconColor').disable({emitEvent: false});
+ this.widgetSettings.get('iconSize').disable({emitEvent: false});
}
- datasourceFormGroup.get('type').valueChanges.subscribe((type: DatasourceType) => {
- datasourceFormGroup.get('entityAliasId').setValidators(
- (type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : []
- );
- const newDataKeysRequired = !this.dataKeysOptional(type);
- datasourceFormGroup.get('dataKeys').setValidators(newDataKeysRequired ? [Validators.required] : []);
- datasourceFormGroup.get('entityAliasId').updateValueAndValidity();
- datasourceFormGroup.get('dataKeys').updateValueAndValidity();
- });
- return datasourceFormGroup;
}
private updateSchemaForm(settings?: any) {
@@ -597,7 +618,13 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
private updateDataSettings() {
if (this.modelValue) {
if (this.modelValue.config) {
- Object.assign(this.modelValue.config, this.dataSettings.value);
+ let data = this.dataSettings.value;
+ if (data.timewindowConfig) {
+ const timewindowConfig: TimewindowConfigData = data.timewindowConfig;
+ data = {...data, ...timewindowConfig};
+ delete data.timewindowConfig;
+ }
+ Object.assign(this.modelValue.config, data);
}
this.propagateChange(this.modelValue);
}
@@ -617,16 +644,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
}
- private updateAlarmSourceSettings() {
- if (this.modelValue) {
- if (this.modelValue.config) {
- const alarmSource: Datasource = this.alarmSourceSettings.value;
- this.modelValue.config.alarmSource = alarmSource;
- }
- this.propagateChange(this.modelValue);
- }
- }
-
private updateWidgetSettings() {
if (this.modelValue) {
if (this.modelValue.config) {
@@ -648,8 +665,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
private updateAdvancedSettings() {
if (this.modelValue) {
if (this.modelValue.config) {
- const settings = this.advancedSettings.get('settings').value?.model;
- this.modelValue.config.settings = settings;
+ this.modelValue.config.settings = this.advancedSettings.get('settings').value?.model;
}
this.propagateChange(this.modelValue);
}
@@ -658,19 +674,30 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
private updateActionSettings() {
if (this.modelValue) {
if (this.modelValue.config) {
- const actions = (this.actionsSettings.get('actionsData').value as WidgetActionsData).actionsMap;
- this.modelValue.config.actions = actions;
+ this.modelValue.config.actions = this.actionsSettings.get('actions').value;
}
this.propagateChange(this.modelValue);
}
}
- public displayAdvanced(): boolean {
+ public get hasBasicModeDirective(): boolean {
+ return this.modelValue?.basicModeDirective?.length > 0;
+ }
+
+ public get useDefinedBasicModeDirective(): boolean {
+ return this.modelValue?.basicModeDirective?.length && !this.basicModeDirectiveError;
+ }
+
+ public get displayAppearance(): boolean {
+ return this.displayAppearanceDataSettings || this.displayAdvancedAppearance;
+ }
+
+ public get displayAdvancedAppearance(): boolean {
return !!this.modelValue && (!!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema ||
!!this.modelValue.settingsDirective && !!this.modelValue.settingsDirective.length);
}
- public displayTimewindowConfig(): boolean {
+ public get displayTimewindowConfig(): boolean {
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) {
return true;
} else if (this.widgetType === widgetType.latest) {
@@ -679,40 +706,30 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
}
- public onlyHistoryTimewindow(): boolean {
- if (this.widgetType === widgetType.latest) {
- const datasources = this.dataSettings.get('datasources').value;
- return datasourcesHasOnlyComparisonAggregation(datasources);
- } else {
- return false;
- }
+ public get displayLimits(): boolean {
+ return this.widgetType !== widgetType.rpc && this.widgetType !== widgetType.alarm &&
+ this.modelValue?.isDataEnabled && !this.modelValue?.typeParameters?.singleEntity;
+ }
+
+ public get displayAppearanceDataSettings(): boolean {
+ return this.displayUnitsConfig || this.displayNoDataDisplayMessageConfig;
}
- public onDatasourceDrop(event: CdkDragDrop) {
- const datasourcesFormArray = this.datasourcesFormArray();
- const datasourceForm = datasourcesFormArray.at(event.previousIndex);
- datasourcesFormArray.removeAt(event.previousIndex);
- datasourcesFormArray.insert(event.currentIndex, datasourceForm);
+ public get displayUnitsConfig(): boolean {
+ return this.widgetType === widgetType.latest || this.widgetType === widgetType.timeseries;
}
- public removeDatasource(index: number) {
- this.datasourcesFormArray().removeAt(index);
+ public get displayNoDataDisplayMessageConfig(): boolean {
+ return this.widgetType !== widgetType.static && !this.modelValue?.typeParameters?.processNoDataByWidget;
}
- public addDatasource() {
- let newDatasource: Datasource;
- if (this.functionsOnly) {
- newDatasource = deepClone(this.utils.getDefaultDatasource(this.modelValue.dataKeySettingsSchema.schema));
- newDatasource.dataKeys = [this.generateDataKey('Sin', DataKeyType.function, this.modelValue.dataKeySettingsSchema)];
+ public onlyHistoryTimewindow(): boolean {
+ if (this.widgetType === widgetType.latest) {
+ const datasources = this.dataSettings.get('datasources').value;
+ return datasourcesHasOnlyComparisonAggregation(datasources);
} else {
- newDatasource = { type: DatasourceType.entity,
- dataKeys: []
- };
- }
- if (this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) {
- newDatasource.latestDataKeys = [];
+ return false;
}
- this.datasourcesFormArray().push(this.buildDatasourceForm(newDatasource));
}
public generateDataKey(chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema): DataKey {
@@ -810,17 +827,26 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
);
}
+ private fetchEntityKeysForDevice(deviceId: string, dataKeyTypes: Array): Observable> {
+ const entityFilter = singleEntityFilterFromDeviceId(deviceId);
+ return this.entityService.getEntityKeysByEntityFilter(
+ entityFilter,
+ dataKeyTypes, [EntityType.DEVICE],
+ {ignoreLoading: true, ignoreErrors: true}
+ ).pipe(
+ catchError(() => of([]))
+ );
+ }
+
private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array): Observable> {
return this.aliasController.getAliasInfo(entityAliasId).pipe(
- mergeMap((aliasInfo) => {
- return this.entityService.getEntityKeysByEntityFilter(
+ mergeMap((aliasInfo) => this.entityService.getEntityKeysByEntityFilter(
aliasInfo.entityFilter,
- dataKeyTypes,
+ dataKeyTypes, [],
{ignoreLoading: true, ignoreErrors: true}
).pipe(
catchError(() => of([]))
- );
- }),
+ )),
catchError(() => of([] as Array))
);
}
@@ -841,9 +867,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
public validate(c: UntypedFormControl) {
- this.timeseriesKeyError = false;
- this.datasourceError = [];
- if (!this.dataSettings.valid) {
+ if (this.basicModeComponent) {
+ if (!this.basicModeComponent.validateConfig()) {
+ return {
+ basicWidgetConfig: {
+ valid: false
+ }
+ };
+ }
+ } else if (!this.dataSettings.valid) {
return {
dataSettings: {
valid: false
@@ -861,7 +893,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
valid: false
}
};
- } else if (!this.advancedSettings.valid || (this.displayAdvanced() && !this.modelValue.config.settings)) {
+ } else if (!this.advancedSettings.valid || (this.displayAdvancedAppearance && !this.modelValue.config.settings)) {
return {
advancedSettings: {
valid: false
@@ -877,54 +909,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
};
}
- } else if (this.widgetType === widgetType.alarm && this.modelValue.isDataEnabled) {
- if (!this.alarmSourceSettings.valid || !config.alarmSource) {
- return {
- alarmSource: {
- valid: false
- }
- };
- }
- } else if (this.widgetType !== widgetType.static && this.modelValue.isDataEnabled) {
- if (!this.modelValue.typeParameters.datasourcesOptional && (!config.datasources || !config.datasources.length)) {
- return {
- datasources: {
- valid: false
- }
- };
- }
- if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) {
- let valid = config.datasources.filter(datasource => datasource?.dataKeys?.length).length > 0;
- if (!valid) {
- this.timeseriesKeyError = true;
- return {
- timeseriesDataKeys: {
- valid: false
- }
- };
- } else {
- const emptyDatasources = config.datasources.filter(datasource => !datasource?.dataKeys?.length &&
- !datasource?.latestDataKeys?.length);
- valid = emptyDatasources.length === 0;
- if (!valid) {
- for (const emptyDatasource of emptyDatasources) {
- const i = config.datasources.indexOf(emptyDatasource);
- this.datasourceError[i] = 'At least one data key should be specified';
- }
- return {
- dataKeys: {
- valid: false
- }
- };
- }
- }
- }
}
}
return null;
}
-
- public extensionPanelIsOpen(value) {
- this.openExtensionPanel = value;
- }
}
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
new file mode 100644
index 0000000000..870cb08513
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss
new file mode 100644
index 0000000000..1782251f61
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016-2023 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.
+ */
+:host {
+ z-index: 10;
+ background: #F3F6FA;
+ .tb-preview-dashboard {
+ position: absolute;
+ top: 15%;
+ bottom: 15%;
+ left: 0;
+ right: 0;
+ }
+ .tb-preview-panel {
+ position: absolute;
+ top: 16px;
+ left: 24px;
+ }
+}
+
+:host ::ng-deep {
+ tb-dashboard.tb-preview-dashboard {
+ .tb-dashboard-content {
+ background-color: transparent !important;
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts
new file mode 100644
index 0000000000..cd088e5744
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts
@@ -0,0 +1,79 @@
+///
+/// Copyright © 2016-2023 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, OnChanges, OnInit, SimpleChanges } from '@angular/core';
+import { PageComponent } from '@shared/components/page.component';
+import { IAliasController, IStateController } from '@core/api/widget-api.models';
+import { Widget, WidgetConfig } from '@shared/models/widget.models';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { deepClone } from '@core/utils';
+
+@Component({
+ selector: 'tb-widget-preview',
+ templateUrl: './widget-preview.component.html',
+ styleUrls: ['./widget-preview.component.scss']
+})
+export class WidgetPreviewComponent extends PageComponent implements OnInit, OnChanges {
+
+ @Input()
+ aliasController: IAliasController;
+
+ @Input()
+ stateController: IStateController;
+
+ @Input()
+ widget: Widget;
+
+ @Input()
+ widgetConfig: WidgetConfig;
+
+ widgets: Widget[];
+
+ constructor(protected store: Store) {
+ super(store);
+ }
+
+ ngOnInit(): void {
+ this.loadPreviewWidget();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ let reloadPreviewWidget = false;
+ for (const propName of Object.keys(changes)) {
+ const change = changes[propName];
+ if (!change.firstChange && change.currentValue !== change.previousValue) {
+ if (['widget', 'widgetConfig'].includes(propName)) {
+ reloadPreviewWidget = true;
+ }
+ }
+ }
+ if (reloadPreviewWidget) {
+ this.loadPreviewWidget();
+ }
+ }
+
+ private loadPreviewWidget() {
+ const widget = deepClone(this.widget);
+ widget.sizeX = 24;
+ widget.sizeY = this.widget.sizeY * 2;
+ widget.row = 0;
+ widget.col = 0;
+ widget.config = this.widgetConfig;
+ this.widgets = [widget];
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.html b/ui-ngx/src/app/modules/home/components/widget/widget.component.html
index ce5a5a57e7..cf287790a0 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.html
@@ -15,23 +15,11 @@
limitations under the License.
-->
-
' + widgetErrorData.message) | safe:'html'">
@@ -42,5 +30,5 @@
class="tb-absolute-fill">{{ noDataDisplayMessageText }}
-
+
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 8424999155..7e9b9cb6e7 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
@@ -39,10 +39,6 @@ import {
} from '@angular/core';
import { DashboardWidget } from '@home/models/dashboard-component.models';
import {
- defaultLegendConfig,
- LegendConfig,
- LegendData,
- LegendPosition,
Widget,
WidgetActionDescriptor,
widgetActionSources,
@@ -152,13 +148,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
displayNoData = false;
noDataDisplayMessageText: string;
- displayLegend: boolean;
- legendConfig: LegendConfig;
- legendData: LegendData;
- isLegendFirst: boolean;
- legendContainerLayoutType: string;
- legendStyle: {[klass: string]: any};
-
dynamicWidgetComponentRef: ComponentRef;
dynamicWidgetComponent: IDynamicWidgetComponent;
@@ -220,57 +209,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widget = this.dashboardWidget.widget;
- this.displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend
- : this.widget.type === widgetType.timeseries;
-
- this.legendContainerLayoutType = 'column';
-
- if (this.displayLegend) {
- this.legendConfig = this.widget.config.legendConfig || defaultLegendConfig(this.widget.type);
- this.legendData = {
- keys: [],
- data: []
- };
- if (this.legendConfig.position === LegendPosition.top ||
- this.legendConfig.position === LegendPosition.bottom) {
- this.legendContainerLayoutType = 'column';
- this.isLegendFirst = this.legendConfig.position === LegendPosition.top;
- } else {
- this.legendContainerLayoutType = 'row';
- this.isLegendFirst = this.legendConfig.position === LegendPosition.left;
- }
- switch (this.legendConfig.position) {
- case LegendPosition.top:
- this.legendStyle = {
- paddingBottom: '8px',
- maxHeight: '50%',
- overflowY: 'auto'
- };
- break;
- case LegendPosition.bottom:
- this.legendStyle = {
- paddingTop: '8px',
- maxHeight: '50%',
- overflowY: 'auto'
- };
- break;
- case LegendPosition.left:
- this.legendStyle = {
- paddingRight: '0px',
- maxWidth: '50%',
- overflowY: 'auto'
- };
- break;
- case LegendPosition.right:
- this.legendStyle = {
- paddingLeft: '0px',
- maxWidth: '50%',
- overflowY: 'auto'
- };
- break;
- }
- }
-
const actionDescriptorsBySourceId: {[actionSourceId: string]: Array} = {};
if (this.widget.config.actions) {
for (const actionSourceId of Object.keys(this.widget.config.actions)) {
@@ -463,13 +401,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
}
- public onLegendKeyHiddenChange(index: number) {
- for (const id of Object.keys(this.widgetContext.subscriptions)) {
- const subscription = this.widgetContext.subscriptions[id];
- subscription.updateDataVisibility(index);
- }
- }
-
private loadFromWidgetInfo() {
this.widgetContext.widgetNamespace =
`widget-type-${(this.widget.isSystemType ? 'sys-' : '')}${this.widget.bundleAlias}-${this.widget.typeAlias}`;
@@ -884,9 +815,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
this.createSubscription(options, subscribe).subscribe(
(subscription) => {
- if (useDefaultComponents) {
- this.defaultSubscriptionOptions(subscription, options);
- }
createSubscriptionSubject.next(subscription);
createSubscriptionSubject.complete();
},
@@ -904,8 +832,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
? this.widget.config.displayTimewindow : !options.useDashboardTimewindow;
options.timeWindowConfig = options.useDashboardTimewindow ? this.widgetContext.dashboardTimewindow : this.widget.config.timewindow;
options.legendConfig = null;
- if (this.displayLegend) {
- options.legendConfig = this.legendConfig;
+ if (this.widget.config.settings.showLegend === true) {
+ options.legendConfig = this.widget.config.settings.legendConfig;
}
options.decimals = this.widgetContext.decimals;
options.units = this.widgetContext.units;
@@ -974,13 +902,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
});
}
};
-
- }
-
- private defaultSubscriptionOptions(subscription: IWidgetSubscription, options: WidgetSubscriptionOptions) {
- if (this.displayLegend) {
- this.legendData = subscription.legendData;
- }
}
private createDefaultSubscription(): Observable {
@@ -1011,7 +932,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.createSubscription(options).subscribe(
(subscription) => {
- this.defaultSubscriptionOptions(subscription, options);
// backward compatibility
this.widgetContext.datasources = subscription.datasources;
@@ -1491,7 +1411,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
}
- private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array, actionDescriptor: WidgetActionDescriptor): Observable {
+ private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array,
+ actionDescriptor: WidgetActionDescriptor): Observable {
const resourceTasks: Observable[] = [];
const modulesTasks: Observable[] = [];
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 8ddb3d6218..c00d0a8e82 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
@@ -447,6 +447,7 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri
}
export interface WidgetConfigComponentData {
+ widgetName: string;
config: WidgetConfig;
layout: WidgetLayout;
widgetType: widgetType;
@@ -459,6 +460,7 @@ export interface WidgetConfigComponentData {
settingsDirective: string;
dataKeySettingsDirective: string;
latestDataKeySettingsDirective: string;
+ basicModeDirective: string;
}
export const MissingWidgetType: WidgetInfo = {
@@ -551,6 +553,8 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
settingsDirective: widgetTypeEntity.descriptor.settingsDirective,
dataKeySettingsDirective: widgetTypeEntity.descriptor.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetTypeEntity.descriptor.latestDataKeySettingsDirective,
+ hasBasicMode: widgetTypeEntity.descriptor.hasBasicMode,
+ basicModeDirective: widgetTypeEntity.descriptor.basicModeDirective,
defaultConfig: widgetTypeEntity.descriptor.defaultConfig
};
}
@@ -581,6 +585,8 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,
+ hasBasicMode: widgetInfo.hasBasicMode,
+ basicModeDirective: widgetInfo.basicModeDirective,
defaultConfig: widgetInfo.defaultConfig
};
return {
diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
index 02a4bf5867..533fd0fea1 100644
--- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
+++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
@@ -28,6 +28,7 @@ import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component';
import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component';
+import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component';
import { QueueComponent } from '@home/pages/admin/queue/queue.component';
import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component';
import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component';
@@ -44,6 +45,7 @@ import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-aut
OAuth2SettingsComponent,
HomeSettingsComponent,
ResourcesLibraryComponent,
+ ResourcesTableHeaderComponent,
QueueComponent,
RepositoryAdminSettingsComponent,
AutoCommitAdminSettingsComponent,
diff --git a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html
index 4524880098..2762e0d43a 100644
--- a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html
+++ b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html
@@ -37,111 +37,306 @@
{{ 'admin.mail-from-required' | translate }}
+
- admin.smtp-protocol
-
-
- {{protocol.toUpperCase()}}
-
-
-
-
-
- admin.smtp-host
-
-
- {{ 'admin.smtp-host-required' | translate }}
-
-
-
- admin.smtp-port
-
- {{smtpPortInput.value?.length || 0}}/5
-
- {{ 'admin.smtp-port-required' | translate }}
-
-
- {{ 'admin.smtp-port-invalid' | translate }}
-
-
-
-
- admin.timeout-msec
-
- {{timeoutInput.value?.length || 0}}/6
-
- {{ 'admin.timeout-required' | translate }}
-
-
- {{ 'admin.timeout-invalid' | translate }}
-
-
-
- {{ 'admin.enable-tls' | translate }}
-
-
- admin.tls-version
-
-
- {{ tlsVersion }}
+ admin.oauth2.smtp-provider
+
+
+ {{ templates.get(provider)?.name || 'Custom' }}
-
- {{ 'admin.enable-proxy' | translate }}
-
-
-
- common.username
-
-
-
- {{ 'admin.change-password' | translate }}
-
-
- common.password
-
-
-
+
+
+ {{ 'admin.oauth2.basic' | translate }}
+ {{ 'admin.oauth2.oauth2' | translate }}
+
+
+
+
+
+
+
+ admin.oauth2.access-token-status
+
+ {{ accessTokenStatus }}
+
+
+
+
+
-
diff --git a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss
index 66df772d2d..cd6c18af9e 100644
--- a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss
+++ b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss
@@ -13,6 +13,95 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@import "../../../../../theme";
+
:host {
+ .fields-group {
+ padding: 0 8px 8px;
+ margin: 10px 0;
+ border: 1px groove rgba(0, 0, 0, .25);
+ border-radius: 4px;
+ legend {
+ margin-bottom: 8px;
+ color: rgba(0, 0, 0, .7);
+ width: fit-content;
+ }
+ }
+
+ .token-status {
+ font: 400 14px / 16px Roboto, "Helvetica Neue", sans-serif;
+ color: rgba(0,0,0, 0.6);
+ letter-spacing: 0.25px;
+ padding: 8px 0;
+ }
+
+ ::ng-deep{
+ .mat-expansion-panel {
+ .mat-expansion-panel-header {
+ height: 48px;
+ padding: 0 12px;
+ &.mat-expanded {
+ height: 48px;
+ }
+ }
+ .mat-expansion-panel-body {
+ padding: 0 12px;
+ }
+ &.configuration-panel {
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ }
+ }
+ .mat-button-toggle-group.tb-notification-unread-toggle-group {
+ &.mat-button-toggle-group-appearance-standard {
+ border: none;
+ border-radius: 14px;
+
+ .mat-button-toggle + .mat-button-toggle {
+ border-left: none;
+ }
+ }
+
+ .mat-button-toggle {
+ background: rgba(0, 0, 0, 0.06);
+ height: 28px;
+ align-items: center;
+ display: flex;
+
+ .mat-button-toggle-ripple {
+ top: 2px;
+ left: 2px;
+ right: 2px;
+ bottom: 2px;
+ border-radius: 12px;
+ }
+ }
+
+ .mat-button-toggle-button {
+ color: #959595;
+ }
+
+ .mat-button-toggle-focus-overlay {
+ border-radius: 14px;
+ margin: 2px;
+ }
+
+ .mat-button-toggle-checked .mat-button-toggle-button {
+ background-color: $tb-primary-color;
+ color: #fff;
+ border-radius: 14px;
+ margin-left: 2px;
+ margin-right: 2px;
+ }
+
+ .mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
+ line-height: 24px;
+ font-size: 16px;
+ font-weight: 500;
+ }
+ .mat-button-toggle-checked.mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay {
+ opacity: .01;
+ }
+ }
+ }
}
diff --git a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts
index 7833c745f4..6815c81e31 100644
--- a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts
+++ b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts
@@ -14,20 +14,32 @@
/// limitations under the License.
///
-import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
-import { Router } from '@angular/router';
-import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
-import { AdminSettings, MailServerSettings, smtpPortPattern } from '@shared/models/settings.models';
+import { FormBuilder, FormGroup, UntypedFormArray, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import {
+ AdminSettings,
+ MailConfigTemplate,
+ MailServerOauth2Provider,
+ MailServerSettings,
+ smtpPortPattern,
+ SmtpProtocol
+} from '@shared/models/settings.models';
import { AdminService } from '@core/http/admin.service';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
-import { isDefinedAndNotNull, isString } from '@core/utils';
-import { Subject } from 'rxjs';
+import { isDefined, isDefinedAndNotNull, isString } from '@core/utils';
+import { forkJoin, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
+import { DomainSchema, domainSchemaTranslations, } from '@shared/models/oauth2.models';
+import { WINDOW } from '@core/services/window.service';
+import { AuthService } from '@core/auth/auth.service';
+import { COMMA, ENTER } from '@angular/cdk/keycodes';
+import { MatChipInputEvent } from '@angular/material/chips';
@Component({
selector: 'tb-mail-server',
@@ -35,40 +47,138 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./mail-server.component.scss', './settings-card.scss']
})
export class MailServerComponent extends PageComponent implements OnInit, OnDestroy, HasConfirmForm {
-
- mailSettings: UntypedFormGroup;
adminSettings: AdminSettings;
- smtpProtocols = ['smtp', 'smtps'];
+ smtpProtocols = Object.values(SmtpProtocol);
showChangePassword = false;
+ protocols = Object.values(DomainSchema).filter(value => value !== DomainSchema.MIXED);
+ domainSchemaTranslations = domainSchemaTranslations;
+
+ mailServerOauth2Provider = MailServerOauth2Provider;
+
tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3'];
+ helpLink: string;
+
+ templates = new Map();
+
+ templateProvider = ['CUSTOM'];
+
+ readonly separatorKeysCodes: number[] = [ENTER, COMMA];
+
private destroy$ = new Subject();
+ private DOMAIN_AND_PORT_REGEXP = /^(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?$/;
+ private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.,?+=&%@\-/]*)?$/;
+ private loginProcessingUrl: string;
+
+ mailSettings = this.fb.group({
+ mailFrom: ['', [Validators.required]],
+ smtpProtocol: [SmtpProtocol.SMTP],
+ smtpHost: ['localhost', [Validators.required]],
+ smtpPort: [25, [Validators.required,
+ Validators.pattern(smtpPortPattern),
+ Validators.maxLength(5)]],
+ timeout: [10000, [Validators.required,
+ Validators.pattern(/^[0-9]{1,6}$/),
+ Validators.maxLength(6)]],
+ enableTls: [false],
+ tlsVersion: [{ value: null, disabled: true }],
+ enableProxy: [false],
+ proxyHost: [{ value: '', disabled: true }, [Validators.required]],
+ proxyPort: [{ value: null, disabled: true }, [Validators.required, Validators.min(1), Validators.max(65535)]],
+ proxyUser: [{ value: '', disabled: true }],
+ proxyPassword: [{ value: '', disabled: true }],
+ username: [''],
+ changePassword: [false],
+ password: [''],
+ enableOauth2: [false],
+ providerId: ['CUSTOM', [Validators.required]],
+ clientId: [{ value:'', disabled: true }, [Validators.required, Validators.maxLength(255)]],
+ clientSecret: [{ value:'', disabled: true }, [Validators.required, Validators.maxLength(2048)]],
+ providerTenantId: [{value: '', disabled: true}, [Validators.required]],
+ authUri: [{value: '', disabled: true}, [Validators.required, Validators.pattern(this.URL_REGEXP)]],
+ tokenUri: [{value: '', disabled: true}, [Validators.required, Validators.pattern(this.URL_REGEXP)]],
+ scope: [],
+ redirectUri: [{ value:'', disabled: true}]
+ });
+
+ private defaultConfiguration = {
+ providerId: 'CUSTOM',
+ smtpProtocol: SmtpProtocol.SMTP,
+ smtpHost: '',
+ smtpPort: null,
+ timeout: null,
+ enableTls: false,
+ tlsVersion: null,
+ enableProxy: false,
+ proxyHost: '',
+ proxyPort: null,
+ proxyUser: '',
+ proxyPassword: '',
+ enableOauth2: false,
+ clientId: '',
+ clientSecret: '',
+ providerTenantId: '',
+ authUri: '',
+ tokenUri: '',
+ scope: [],
+ redirectUri: ''
+ };
+
+ domainForm = this.fb.group({
+ name: [this.window.location.hostname, [
+ Validators.required, Validators.maxLength(255),
+ Validators.pattern(this.DOMAIN_AND_PORT_REGEXP)]
+ ],
+ scheme: [DomainSchema.HTTPS, Validators.required]
+ });
constructor(protected store: Store,
private router: Router,
+ private route: ActivatedRoute,
private adminService: AdminService,
+ private authService: AuthService,
private translate: TranslateService,
- public fb: UntypedFormBuilder) {
+ public fb: FormBuilder,
+ @Inject(WINDOW) private window: Window) {
super(store);
}
ngOnInit() {
- this.buildMailServerSettingsForm();
- this.adminService.getAdminSettings('mail').subscribe(
- (adminSettings) => {
- this.adminSettings = adminSettings;
- if (this.adminSettings.jsonValue && isString(this.adminSettings.jsonValue.enableTls)) {
- this.adminSettings.jsonValue.enableTls = (this.adminSettings.jsonValue.enableTls as any) === 'true';
- }
- this.showChangePassword =
- isDefinedAndNotNull(this.adminSettings.jsonValue.showChangePassword) ? this.adminSettings.jsonValue.showChangePassword : true ;
- delete this.adminSettings.jsonValue.showChangePassword;
- this.mailSettings.reset(this.adminSettings.jsonValue);
- this.enableMailPassword(!this.showChangePassword);
- this.enableProxyChanged();
+ this.mailServerSettingsForm();
+ this.domainFormConfiguration();
+
+ forkJoin([
+ this.adminService.getLoginProcessingUrl(),
+ this.adminService.getMailConfigTemplate(),
+ this.adminService.getAdminSettings('mail')
+ ]).subscribe(([loginProcessingUrl, mailConfigTemplate, adminSettings]) => {
+ this.loginProcessingUrl = loginProcessingUrl;
+ this.initTemplates(mailConfigTemplate);
+ this.adminSettings = adminSettings;
+ if (this.adminSettings.jsonValue && isString(this.adminSettings.jsonValue.enableTls)) {
+ this.adminSettings.jsonValue.enableTls = (this.adminSettings.jsonValue.enableTls as any) === 'true';
}
- );
+ this.showChangePassword = isDefinedAndNotNull(this.adminSettings.jsonValue.showChangePassword)
+ ? this.adminSettings.jsonValue.showChangePassword : true;
+ delete this.adminSettings.jsonValue.showChangePassword;
+ if (!this.adminSettings.jsonValue.providerId) {
+ this.adminSettings.jsonValue.providerId = 'CUSTOM';
+ }
+ this.mailSettings.reset(this.adminSettings.jsonValue, {emitEvent: false});
+ this.enableMailPassword(!this.showChangePassword);
+ this.enableProxyChanged();
+ this.enableTls(this.adminSettings.jsonValue.enableTls);
+ this.helpLink = this.templates.get(this.adminSettings.jsonValue.providerId)?.helpLink || null;
+ if (this.adminSettings.jsonValue.enableOauth2) {
+ this.enableOauth2(!!this.adminSettings.jsonValue.enableOauth2);
+ this.enableProviderTenantIdChanged(this.adminSettings.jsonValue.providerId);
+ this.parseUrl(this.adminSettings.jsonValue.redirectUri);
+ this.mailSettings.get('redirectUri').patchValue(this.adminSettings.jsonValue.redirectUri, {emitEvent: false});
+ } else {
+ this.mailSettings.get('enableOauth2').patchValue(false, {emitEvent: false});
+ }
+ });
}
ngOnDestroy() {
@@ -77,56 +187,153 @@ export class MailServerComponent extends PageComponent implements OnInit, OnDest
super.ngOnDestroy();
}
- buildMailServerSettingsForm() {
- this.mailSettings = this.fb.group({
- mailFrom: ['', [Validators.required]],
- smtpProtocol: ['smtp'],
- smtpHost: ['localhost', [Validators.required]],
- smtpPort: ['25', [Validators.required,
- Validators.pattern(smtpPortPattern),
- Validators.maxLength(5)]],
- timeout: ['10000', [Validators.required,
- Validators.pattern(/^[0-9]{1,6}$/),
- Validators.maxLength(6)]],
- enableTls: [false],
- tlsVersion: [],
- enableProxy: [false, []],
- proxyHost: ['', [Validators.required]],
- proxyPort: ['', [Validators.required, Validators.min(1), Validators.max(65535)]],
- proxyUser: [''],
- proxyPassword: [''],
- username: [''],
- changePassword: [false],
- password: ['']
+ private initTemplates(templates): void {
+ templates.map(provider => {
+ delete provider.additionalInfo;
+ this.templates.set(provider.providerId, provider);
});
+ this.templateProvider.push(...Array.from(this.templates.keys()));
+ this.templateProvider.sort();
+ }
+
+ private mailServerSettingsForm(): void {
this.registerDisableOnLoadFormControl(this.mailSettings.get('smtpProtocol'));
this.registerDisableOnLoadFormControl(this.mailSettings.get('enableTls'));
this.registerDisableOnLoadFormControl(this.mailSettings.get('enableProxy'));
this.registerDisableOnLoadFormControl(this.mailSettings.get('changePassword'));
+
+ this.mailSettings.get('enableTls').valueChanges.pipe(
+ takeUntil(this.destroy$)
+ ).subscribe(value => this.enableTls(value));
+
this.mailSettings.get('enableProxy').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.enableProxyChanged();
});
+
this.mailSettings.get('changePassword').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.enableMailPassword(value);
});
+
+ this.mailSettings.get('enableOauth2').valueChanges.pipe(
+ takeUntil(this.destroy$)
+ ).subscribe( value => {
+ this.enableOauth2(value);
+ this.enableProviderTenantIdChanged(this.mailSettings.get('providerId').value);
+ if (value && !this.mailSettings.get('redirectUri').value) {
+ this.mailSettings.get('redirectUri').patchValue(this.redirectURI(), {emitEvent: false});
+ }
+ });
+
+ this.mailSettings.get('providerTenantId').valueChanges.pipe(
+ takeUntil(this.destroy$)
+ ).subscribe(tenantId => {
+ const authorizationUri = this.templates.get(this.mailServerOauth2Provider.OFFICE_365).authorizationUri.replace('%s', `${tenantId}`);
+ const accessTokenUri = this.templates.get(this.mailServerOauth2Provider.OFFICE_365).accessTokenUri.replace('%s', `${tenantId}`);
+ this.mailSettings.get('authUri').patchValue(authorizationUri, {emitEvent: false});
+ this.mailSettings.get('tokenUri').patchValue(accessTokenUri, {emitEvent: false});
+ });
+
+ this.mailSettings.get('providerId').valueChanges.pipe(
+ takeUntil(this.destroy$)
+ ).subscribe( value => {
+ if (value === this.mailServerOauth2Provider.CUSTOM || !value) {
+ this.mailSettings.reset({...this.adminSettings.jsonValue, ...this.defaultConfiguration}, {emitEvent: false});
+ } else {
+ const config = this.templates.get(value);
+ this.helpLink = config.helpLink;
+ this.mailSettings.patchValue({
+ smtpProtocol: SmtpProtocol[config.smtpProtocol],
+ smtpHost: config.smtpHost,
+ smtpPort: config.smtpPort,
+ timeout: config.timeout,
+ enableTls: config.enableTls,
+ tlsVersion: config.tlsVersion,
+ authUri: config.authorizationUri,
+ tokenUri: config.accessTokenUri,
+ scope: config.scope,
+ enableOauth2: false,
+ enableProxy: false,
+ proxyHost: '',
+ proxyPort: null,
+ proxyUser: '',
+ proxyPassword: '',
+ clientId: '',
+ clientSecret: '',
+ providerTenantId: '',
+ redirectUri: ''
+ }, {emitEvent: false});
+ }
+ this.enableTls(this.mailSettings.get('enableTls').value);
+ this.enableOauth2(this.mailSettings.get('enableOauth2').value);
+ this.enableProviderTenantIdChanged(value);
+ });
+ }
+
+ private domainFormConfiguration(): void {
+ this.domainForm.get('name').valueChanges.pipe(
+ takeUntil(this.destroy$)
+ ).subscribe(
+ value => this.mailSettings.get('redirectUri').patchValue(
+ this.redirectURI(this.domainForm.get('scheme').value, value),
+ {emitEvent: false}
+ )
+ );
+ this.domainForm.get('scheme').valueChanges.pipe(
+ takeUntil(this.destroy$)
+ ).subscribe(
+ (value) => this.mailSettings.get('redirectUri').patchValue(this.redirectURI(value), {emitEvent: false})
+ );
}
- enableProxyChanged(): void {
+ private enableOauth2(value: boolean): void {
+ if (value) {
+ this.mailSettings.get('clientId').enable({emitEvent: false});
+ this.mailSettings.get('clientSecret').enable({emitEvent: false});
+ this.mailSettings.get('redirectUri').enable({emitEvent: false});
+ if (this.mailSettings.get('providerId').value === this.mailServerOauth2Provider.CUSTOM) {
+ this.mailSettings.get('authUri').enable({emitEvent: false});
+ this.mailSettings.get('tokenUri').enable({emitEvent: false});
+ } else {
+ this.mailSettings.get('authUri').disable({emitEvent: false});
+ this.mailSettings.get('tokenUri').disable({emitEvent: false});
+ }
+ } else {
+ this.mailSettings.get('clientId').disable({emitEvent: false});
+ this.mailSettings.get('clientSecret').disable({emitEvent: false});
+ this.mailSettings.get('redirectUri').disable({emitEvent: false});
+ this.mailSettings.get('authUri').disable({emitEvent: false});
+ this.mailSettings.get('tokenUri').disable({emitEvent: false});
+ }
+ }
+
+ private enableProviderTenantIdChanged(value: string): void {
+ if (value === this.mailServerOauth2Provider.OFFICE_365 && this.mailSettings.get('enableOauth2').value) {
+ this.mailSettings.get('providerTenantId').enable({emitEvent: false});
+ } else {
+ this.mailSettings.get('providerTenantId').disable({emitEvent: false});
+ }
+ }
+
+ private enableProxyChanged(): void {
const enableProxy: boolean = this.mailSettings.get('enableProxy').value;
if (enableProxy) {
- this.mailSettings.get('proxyHost').enable();
- this.mailSettings.get('proxyPort').enable();
+ this.mailSettings.get('proxyHost').enable({emitEvent: false});
+ this.mailSettings.get('proxyPort').enable({emitEvent: false});
+ this.mailSettings.get('proxyUser').enable({emitEvent: false});
+ this.mailSettings.get('proxyPassword').enable({emitEvent: false});
} else {
- this.mailSettings.get('proxyHost').disable();
- this.mailSettings.get('proxyPort').disable();
+ this.mailSettings.get('proxyHost').disable({emitEvent: false});
+ this.mailSettings.get('proxyPort').disable({emitEvent: false});
+ this.mailSettings.get('proxyUser').disable({emitEvent: false});
+ this.mailSettings.get('proxyPassword').disable({emitEvent: false});
}
}
- enableMailPassword(enable: boolean) {
+ private enableMailPassword(enable: boolean) {
if (enable) {
this.mailSettings.get('password').enable({emitEvent: false});
} else {
@@ -134,14 +341,21 @@ export class MailServerComponent extends PageComponent implements OnInit, OnDest
}
}
+ private enableTls(enable: boolean): void {
+ if (enable) {
+ this.mailSettings.get('tlsVersion').enable({emitEvent: false});
+ } else {
+ this.mailSettings.get('tlsVersion').disable({emitEvent: false});
+ }
+ }
+
sendTestMail(): void {
this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettingsFormValue};
- this.adminService.sendTestMail(this.adminSettings).subscribe(
- () => {
- this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.test-mail-sent'),
- type: 'success' }));
- }
- );
+ this.adminService.sendTestMail(this.adminSettings).subscribe({
+ next: () => this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.test-mail-sent'),
+ type: 'success' })),
+ error: error => this.store.dispatch(new ActionNotificationShow({message: error.error.message, type: 'error'}))
+ });
}
save(): void {
@@ -150,18 +364,88 @@ export class MailServerComponent extends PageComponent implements OnInit, OnDest
(adminSettings) => {
this.adminSettings = adminSettings;
this.showChangePassword = true;
- this.mailSettings.reset(this.adminSettings.jsonValue);
+ this.mailSettings.reset(this.adminSettings.jsonValue, {emitEvent: false});
+ this.domainForm.reset(this.domainForm.value);
+ this.parseUrl(this.adminSettings.jsonValue.redirectUri);
}
);
}
- confirmForm(): UntypedFormGroup {
+ generateAccessToken(): void {
+ this.adminService.generateAccessToken().subscribe(
+ uri => this.window.location.href = uri
+ );
+ }
+
+ redirectURI(schema?: DomainSchema, name?: string): string {
+ const domainInfo = this.domainForm.value;
+ if (domainInfo.name !== '') {
+ const protocol = isDefined(schema) ? schema.toLowerCase() : domainInfo.scheme.toLowerCase();
+ const domainName = isDefined(name) ? name : domainInfo.name;
+ return `${protocol}://${domainName}${this.loginProcessingUrl}`;
+ }
+ return '';
+ }
+
+ private parseUrl(value: string): void {
+ if (value) {
+ const url = new URL(value);
+ this.domainForm.get('scheme').patchValue(
+ url.protocol.startsWith('https') ? DomainSchema.HTTPS : DomainSchema.HTTP, {emitEvent: false}
+ );
+ this.domainForm.get('name').patchValue(url.host, {emitEvent: false});
+ }
+ }
+
+ get accessTokenButtonName(): string {
+ return this.translate.instant(
+ this.adminSettings.jsonValue.tokenGenerated ? 'admin.oauth2.update-access-token' : 'admin.oauth2.generate-access-token'
+ );
+ }
+
+ get accessTokenStatus(): string {
+ return this.translate.instant(
+ this.adminSettings.jsonValue.tokenGenerated ? 'admin.oauth2.token-status-generated' : 'admin.oauth2.token-status-not-generated'
+ );
+ }
+
+ confirmForm(): FormGroup {
return this.mailSettings;
}
private get mailSettingsFormValue(): MailServerSettings {
- const formValue = this.mailSettings.value;
+ const formValue = this.mailSettings.getRawValue() as Required;
delete formValue.changePassword;
return formValue;
}
+
+ trackByParams(index: number): number {
+ return index;
+ }
+
+ removeScope(i: number): void {
+ const controller = this.mailSettings.get('scope') as UntypedFormArray;
+ controller.removeAt(i);
+ controller.markAsTouched();
+ controller.markAsDirty();
+ }
+
+ addScope(event: MatChipInputEvent): void {
+ const input = event.chipInput.inputElement;
+ const value = event.value;
+ const controller = this.mailSettings.get('scope') as UntypedFormArray;
+ if ((value.trim() !== '')) {
+ controller.push(this.fb.control(value.trim()));
+ controller.markAsDirty();
+ }
+
+ if (input) {
+ input.value = '';
+ }
+ }
+
+ toggleEditMode(path: string): void {
+ this.mailSettings.get(path).disabled ? this.mailSettings.get(path).enable() : this.mailSettings.get(path).disable();
+ }
+
}
diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
index a488bd3807..733e5ef2b3 100644
--- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
+++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
@@ -36,6 +36,7 @@ import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-
import { PageLink } from '@shared/models/page/page-link';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { map } from 'rxjs/operators';
+import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component';
@Injectable()
export class ResourcesLibraryTableConfigResolver implements Resolve> {
@@ -53,6 +54,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve resource ?
resource.title : '';
@@ -61,11 +63,9 @@ export class ResourcesLibraryTableConfigResolver implements Resolve('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn('title', 'resource.title', '60%'),
new EntityTableColumn