Merge branch 'develop/3.5.2' into feature/basic-widget-config

This commit is contained in:
Igor Kulikov 2023-06-06 18:02:25 +03:00
commit c6dd11f576
21 changed files with 468 additions and 68 deletions

View File

@ -300,6 +300,7 @@ import * as QueueFormComponent from '@home/components/queue/queue-form.component
import * as AssetProfileComponent from '@home/components/profile/asset-profile.component'; import * as AssetProfileComponent from '@home/components/profile/asset-profile.component';
import * as AssetProfileDialogComponent from '@home/components/profile/asset-profile-dialog.component'; import * as AssetProfileDialogComponent from '@home/components/profile/asset-profile-dialog.component';
import * as AssetProfileAutocompleteComponent from '@home/components/profile/asset-profile-autocomplete.component'; import * as AssetProfileAutocompleteComponent from '@home/components/profile/asset-profile-autocomplete.component';
import * as RuleChainSelectComponent from '@shared/components/rule-chain/rule-chain-select.component';
import { IModulesMap } from '@modules/common/modules-map.models'; import { IModulesMap } from '@modules/common/modules-map.models';
@ -418,6 +419,7 @@ class ModulesMap implements IModulesMap {
'@shared/components/time/quick-time-interval.component': QuickTimeIntervalComponent, '@shared/components/time/quick-time-interval.component': QuickTimeIntervalComponent,
'@shared/components/dashboard-select.component': DashboardSelectComponent, '@shared/components/dashboard-select.component': DashboardSelectComponent,
'@shared/components/dashboard-select-panel.component': DashboardSelectPanelComponent, '@shared/components/dashboard-select-panel.component': DashboardSelectPanelComponent,
'@shared/components/rule-chain/rule-chain-select.component': RuleChainSelectComponent,
'@shared/components/time/datetime-period.component': DatetimePeriodComponent, '@shared/components/time/datetime-period.component': DatetimePeriodComponent,
'@shared/components/time/datetime.component': DatetimeComponent, '@shared/components/time/datetime.component': DatetimeComponent,
'@shared/components/time/timezone-select.component': TimezoneSelectComponent, '@shared/components/time/timezone-select.component': TimezoneSelectComponent,

View File

@ -66,4 +66,29 @@
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </fieldset>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.default-column-visibility</mat-label>
<mat-select formControlName="defaultColumnVisibility">
<mat-option [value]="'visible'">
{{ 'widgets.table.column-visibility-visible' | translate }}
</mat-option>
<mat-option [value]="'hidden'">
{{ 'widgets.table.column-visibility-hidden' | translate }}
</mat-option>
<mat-option [value]="'hidden-mobile'">
{{ 'widgets.table.column-visibility-hidden-mobile' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.column-selection-to-display</mat-label>
<mat-select formControlName="columnSelectionToDisplay">
<mat-option [value]="'enabled'">
{{ 'widgets.table.column-selection-to-display-enabled' | translate }}
</mat-option>
<mat-option [value]="'disabled'">
{{ 'widgets.table.column-selection-to-display-disabled' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</section> </section>

View File

@ -43,7 +43,9 @@ export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent
useCellStyleFunction: false, useCellStyleFunction: false,
cellStyleFunction: '', cellStyleFunction: '',
useCellContentFunction: false, useCellContentFunction: false,
cellContentFunction: '' cellContentFunction: '',
defaultColumnVisibility: 'visible',
columnSelectionToDisplay: 'enabled'
}; };
} }
@ -53,6 +55,8 @@ export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent
cellStyleFunction: [settings.cellStyleFunction, [Validators.required]], cellStyleFunction: [settings.cellStyleFunction, [Validators.required]],
useCellContentFunction: [settings.useCellContentFunction, []], useCellContentFunction: [settings.useCellContentFunction, []],
cellContentFunction: [settings.cellContentFunction, [Validators.required]], cellContentFunction: [settings.cellContentFunction, [Validators.required]],
defaultColumnVisibility: [settings.defaultColumnVisibility, []],
columnSelectionToDisplay: [settings.columnSelectionToDisplay, []],
}); });
} }

View File

@ -73,4 +73,29 @@
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </fieldset>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.default-column-visibility</mat-label>
<mat-select formControlName="defaultColumnVisibility">
<mat-option [value]="'visible'">
{{ 'widgets.table.column-visibility-visible' | translate }}
</mat-option>
<mat-option [value]="'hidden'">
{{ 'widgets.table.column-visibility-hidden' | translate }}
</mat-option>
<mat-option [value]="'hidden-mobile'">
{{ 'widgets.table.column-visibility-hidden-mobile' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.column-selection-to-display</mat-label>
<mat-select formControlName="columnSelectionToDisplay">
<mat-option [value]="'enabled'">
{{ 'widgets.table.column-selection-to-display-enabled' | translate }}
</mat-option>
<mat-option [value]="'disabled'">
{{ 'widgets.table.column-selection-to-display-disabled' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</section> </section>

View File

@ -44,7 +44,9 @@ export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsCom
useCellStyleFunction: false, useCellStyleFunction: false,
cellStyleFunction: '', cellStyleFunction: '',
useCellContentFunction: false, useCellContentFunction: false,
cellContentFunction: '' cellContentFunction: '',
defaultColumnVisibility: 'visible',
columnSelectionToDisplay: 'enabled'
}; };
} }
@ -56,6 +58,8 @@ export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsCom
cellStyleFunction: [settings.cellStyleFunction, [Validators.required]], cellStyleFunction: [settings.cellStyleFunction, [Validators.required]],
useCellContentFunction: [settings.useCellContentFunction, []], useCellContentFunction: [settings.useCellContentFunction, []],
cellContentFunction: [settings.cellContentFunction, [Validators.required]], cellContentFunction: [settings.cellContentFunction, [Validators.required]],
defaultColumnVisibility: [settings.defaultColumnVisibility, []],
columnSelectionToDisplay: [settings.columnSelectionToDisplay, []],
}); });
} }

View File

@ -23,6 +23,9 @@
<mat-checkbox formControlName="enableSearch"> <mat-checkbox formControlName="enableSearch">
{{ 'widgets.table.enable-search' | translate }} {{ 'widgets.table.enable-search' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox formControlName="enableSelectColumnDisplay">
{{ 'widgets.table.enable-select-column-display' | translate }}
</mat-checkbox>
</section> </section>
<section fxLayout="column" fxFlex> <section fxLayout="column" fxFlex>
<mat-checkbox formControlName="enableStickyHeader"> <mat-checkbox formControlName="enableStickyHeader">

View File

@ -41,6 +41,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
protected defaultSettings(): WidgetSettings { protected defaultSettings(): WidgetSettings {
return { return {
enableSearch: true, enableSearch: true,
enableSelectColumnDisplay: true,
enableStickyHeader: true, enableStickyHeader: true,
enableStickyAction: true, enableStickyAction: true,
reserveSpaceForHiddenAction: 'true', reserveSpaceForHiddenAction: 'true',
@ -59,6 +60,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
protected onSettingsSet(settings: WidgetSettings) { protected onSettingsSet(settings: WidgetSettings) {
this.timeseriesTableWidgetSettingsForm = this.fb.group({ this.timeseriesTableWidgetSettingsForm = this.fb.group({
enableSearch: [settings.enableSearch, []], enableSearch: [settings.enableSearch, []],
enableSelectColumnDisplay: [settings.enableSelectColumnDisplay, []],
enableStickyHeader: [settings.enableStickyHeader, []], enableStickyHeader: [settings.enableStickyHeader, []],
enableStickyAction: [settings.enableStickyAction, []], enableStickyAction: [settings.enableStickyAction, []],
reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []], reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []],

View File

@ -31,6 +31,7 @@ type ColumnSelectionOptions = 'enabled' | 'disabled';
export interface TableWidgetSettings { export interface TableWidgetSettings {
enableSearch: boolean; enableSearch: boolean;
enableSelectColumnDisplay: boolean;
enableStickyAction: boolean; enableStickyAction: boolean;
enableStickyHeader: boolean; enableStickyHeader: boolean;
displayPagination: boolean; displayPagination: boolean;

View File

@ -19,9 +19,12 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
Injector,
Input, Input,
OnDestroy,
OnInit, OnInit,
QueryList, QueryList,
StaticProvider,
ViewChild, ViewChild,
ViewChildren, ViewChildren,
ViewContainerRef ViewContainerRef
@ -64,8 +67,9 @@ import {
CellStyleInfo, CellStyleInfo,
checkHasActions, checkHasActions,
constructTableCssString, constructTableCssString,
DisplayColumn,
getCellContentInfo, getCellContentInfo,
getCellStyleInfo, getCellStyleInfo, getColumnDefaultVisibility, getColumnSelectionAvailability,
getRowStyleInfo, getRowStyleInfo,
getTableCellButtonActions, getTableCellButtonActions,
noDataMessage, noDataMessage,
@ -75,12 +79,17 @@ import {
TableWidgetDataKeySettings, TableWidgetDataKeySettings,
TableWidgetSettings TableWidgetSettings
} from '@home/components/widget/lib/table-widget.models'; } from '@home/components/widget/lib/table-widget.models';
import { Overlay } from '@angular/cdk/overlay'; import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; import { SubscriptionEntityInfo } from '@core/api/widget-api.models';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants'; import { hidePageSizePixelValue } from '@shared/models/constants';
import {
DISPLAY_COLUMNS_PANEL_DATA,
DisplayColumnsPanelComponent
} from '@home/components/widget/lib/display-columns-panel.component';
import { ComponentPortal } from '@angular/cdk/portal';
export interface TimeseriesTableWidgetSettings extends TableWidgetSettings { export interface TimeseriesTableWidgetSettings extends TableWidgetSettings {
showTimestamp: boolean; showTimestamp: boolean;
@ -105,6 +114,8 @@ interface TimeseriesHeader {
dataKey: DataKey; dataKey: DataKey;
sortable: boolean; sortable: boolean;
show: boolean; show: boolean;
columnDefaultVisibility?: boolean;
columnSelectionAvailability?: boolean;
styleInfo: CellStyleInfo; styleInfo: CellStyleInfo;
contentInfo: CellContentInfo; contentInfo: CellContentInfo;
order?: number; order?: number;
@ -131,7 +142,7 @@ interface TimeseriesTableSource {
templateUrl: './timeseries-table-widget.component.html', templateUrl: './timeseries-table-widget.component.html',
styleUrls: ['./timeseries-table-widget.component.scss', './table-widget.scss'] styleUrls: ['./timeseries-table-widget.component.scss', './table-widget.scss']
}) })
export class TimeseriesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit { export class TimeseriesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy {
@Input() @Input()
ctx: WidgetContext; ctx: WidgetContext;
@ -169,6 +180,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
private useEntityLabel = false; private useEntityLabel = false;
private dateFormatFilter: string; private dateFormatFilter: string;
private displayedColumns: Array<DisplayColumn[]> = [];
private rowStylesInfo: RowStyleInfo; private rowStylesInfo: RowStyleInfo;
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
@ -184,6 +197,15 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
} }
}; };
private columnDisplayAction: WidgetAction = {
name: 'entity.columns-to-display',
show: true,
icon: 'view_column',
onAction: ($event) => {
this.editColumnsToDisplay($event);
}
};
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private elementRef: ElementRef, private elementRef: ElementRef,
private overlay: Overlay, private overlay: Overlay,
@ -275,11 +297,12 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
} }
private initialize() { private initialize() {
this.ctx.widgetActions = [this.searchAction ]; this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction];
this.setCellButtonAction = !!this.ctx.actionsApi.getActionDescriptors('actionCellButton').length; this.setCellButtonAction = !!this.ctx.actionsApi.getActionDescriptors('actionCellButton').length;
this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true; this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true;
this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true;
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true; this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true;
this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true; this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true;
@ -365,9 +388,82 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.sources.push(source); this.sources.push(source);
} }
} }
this.prepareDisplayedColumn();
this.sources[this.sourceIndex].displayedColumns =
this.displayedColumns[this.sourceIndex].filter(value => value.display).map(value => value.def);
this.updateActiveEntityInfo(); this.updateActiveEntityInfo();
} }
private editColumnsToDisplay($event: Event) {
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const source = this.sources[this.sourceIndex];
this.prepareDisplayedColumn();
const providers: StaticProvider[] = [
{
provide: DISPLAY_COLUMNS_PANEL_DATA,
useValue: {
columns: this.displayedColumns[this.sourceIndex],
columnsUpdated: (newColumns) => {
source.displayedColumns = newColumns.filter(value => value.display).map(value => value.def);
this.clearCache();
}
}
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
this.viewContainerRef, injector));
this.ctx.detectChanges();
}
private prepareDisplayedColumn() {
if (!this.displayedColumns[this.sourceIndex]) {
this.displayedColumns[this.sourceIndex] = this.sources[this.sourceIndex].displayedColumns.map(value => {
let title = '';
const header = this.sources[this.sourceIndex].header.find(column => column.index.toString() === value);
if (value === '0') {
title = 'Timestamp';
} else if (value === 'actions') {
title = 'Actions';
} else {
title = header.dataKey.name;
}
return {
title,
def: value,
display: header?.columnDefaultVisibility ?? true,
selectable: header?.columnSelectionAvailability ?? true
};
});
}
}
private prepareHeader(datasource: Datasource): TimeseriesHeader[] { private prepareHeader(datasource: Datasource): TimeseriesHeader[] {
const dataKeys = datasource.dataKeys; const dataKeys = datasource.dataKeys;
const latestDataKeys = datasource.latestDataKeys; const latestDataKeys = datasource.latestDataKeys;
@ -377,6 +473,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
const keySettings: TableWidgetDataKeySettings = dataKey.settings; const keySettings: TableWidgetDataKeySettings = dataKey.settings;
const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx'); const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx');
const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx'); const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx');
const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx);
const columnSelectionAvailability = getColumnSelectionAvailability(keySettings);
contentInfo.units = dataKey.units; contentInfo.units = dataKey.units;
contentInfo.decimals = dataKey.decimals; contentInfo.decimals = dataKey.decimals;
header.push({ header.push({
@ -386,6 +484,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
styleInfo, styleInfo,
contentInfo, contentInfo,
show: true, show: true,
columnDefaultVisibility,
columnSelectionAvailability,
order: index + 2 order: index + 2
}); });
}); });
@ -396,6 +496,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
const keySettings: TimeseriesWidgetLatestDataKeySettings = dataKey.settings; const keySettings: TimeseriesWidgetLatestDataKeySettings = dataKey.settings;
const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx'); const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx');
const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx'); const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx');
const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx);
const columnSelectionAvailability = getColumnSelectionAvailability(keySettings);
contentInfo.units = dataKey.units; contentInfo.units = dataKey.units;
contentInfo.decimals = dataKey.decimals; contentInfo.decimals = dataKey.decimals;
header.push({ header.push({
@ -405,13 +507,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
styleInfo, styleInfo,
contentInfo, contentInfo,
show: isDefinedAndNotNull(keySettings.show) ? keySettings.show : true, show: isDefinedAndNotNull(keySettings.show) ? keySettings.show : true,
columnDefaultVisibility,
columnSelectionAvailability,
order: isDefinedAndNotNull(keySettings.order) ? keySettings.order : (index + 2) order: isDefinedAndNotNull(keySettings.order) ? keySettings.order : (index + 2)
}); });
}); });
} }
header = header.sort((a, b) => { header = header.sort((a, b) => a.order - b.order);
return a.order - b.order;
});
return header; return header;
} }

View File

@ -59,8 +59,7 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<tb-template-autocomplete <tb-template-autocomplete
required required allowCreate allowEdit
allowCreate
formControlName="templateId" formControlName="templateId"
[notificationTypes]="ruleNotificationForm.get('triggerType').value"> [notificationTypes]="ruleNotificationForm.get('triggerType').value">
</tb-template-autocomplete> </tb-template-autocomplete>

View File

@ -49,8 +49,7 @@
</div> </div>
<div *ngIf="notificationRequestForm.get('useTemplate').value; else scratchTemplate"> <div *ngIf="notificationRequestForm.get('useTemplate').value; else scratchTemplate">
<tb-template-autocomplete <tb-template-autocomplete
required required allowCreate allowEdit
allowCreate
formControlName="templateId" formControlName="templateId"
[notificationTypes]="notificationType.GENERAL"> [notificationTypes]="notificationType.GENERAL">
</tb-template-autocomplete> </tb-template-autocomplete>

View File

@ -15,55 +15,48 @@
limitations under the License. limitations under the License.
--> -->
<div class="mat-content" fxFlex tb-fullscreen [fullscreen]="isFullscreen" tb-hotkeys [hotkeys]="hotKeys" <div class="mat-content tb-rulechain" fxFlex tb-fullscreen [fullscreen]="isFullscreen" tb-hotkeys [hotkeys]="hotKeys"
[cheatSheet]="cheatSheetComponent" [cheatSheet]="cheatSheetComponent" fxLayout="column">
fxLayout="column" class="tb-rulechain">
<tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet> <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
<section class="tb-rulechain-container" fxFlex fxLayout="column"> <section class="tb-rulechain-container" fxFlex fxLayout="column">
<div class="tb-rulechain-layout" fxFlex fxLayout="row"> <div class="tb-rulechain-layout" fxFlex fxLayout="row">
<section fxLayout="row"
class="tb-header-buttons tb-library-open" [fxShow]="!isLibraryOpen">
<button color="primary"
mat-mini-fab
class="tb-btn-header tb-btn-open-library"
(click)="isLibraryOpen = true"
matTooltip="{{ 'rulenode.open-node-library' | translate }}"
matTooltipPosition="above">
<mat-icon>menu</mat-icon>
</button>
</section>
<mat-drawer-container style="width: 100%; height: 100%;"> <mat-drawer-container style="width: 100%; height: 100%;">
<mat-drawer class="tb-rulechain-library mat-elevation-z4" <mat-drawer class="tb-rulechain-library mat-elevation-z4"
disableClose="true" disableClose="true"
mode="side" mode="side"
[opened]="isLibraryOpen" #drawer
opened
position="start" position="start"
fxLayout="column"> fxLayout="column">
<mat-toolbar color="primary" class="tb-dark"> <mat-toolbar color="primary" class="tb-dark">
<tb-rule-chain-select
fxFlex
*ngIf="!isImport"
[ruleChainType]="ruleChainType"
[disabled]="isDirtyValue"
[(ngModel)]="ruleChain.id.id"
(ngModelChange)="currentRuleChainIdChanged(ruleChain.id?.id)">
</tb-rule-chain-select>
</mat-toolbar>
<mat-toolbar>
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<button mat-icon-button class="tb-small" <mat-form-field fxFlex class="tb-appearance-transparent">
<button mat-icon-button matPrefix class="tb-small"
matTooltip="{{'rulenode.search' | translate}}" matTooltip="{{'rulenode.search' | translate}}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>search</mat-icon> <mat-icon>search</mat-icon>
</button> </button>
<mat-form-field fxFlex class="tb-appearance-transparent">
<input #ruleNodeSearchInput matInput <input #ruleNodeSearchInput matInput
[(ngModel)]="ruleNodeTypeSearch" [(ngModel)]="ruleNodeTypeSearch"
placeholder="{{'rulenode.search' | translate}}"/> placeholder="{{'rulenode.search' | translate}}"/>
</mat-form-field> <button mat-icon-button matSuffix class="tb-small"
<button mat-icon-button class="tb-small"
[fxShow]="ruleNodeTypeSearch !== ''" [fxShow]="ruleNodeTypeSearch !== ''"
(click)="ruleNodeTypeSearch = ''; updateRuleChainLibrary()" (click)="ruleNodeTypeSearch = ''; updateRuleChainLibrary()"
matTooltip="{{'action.clear-search' | translate}}" matTooltip="{{'action.clear-search' | translate}}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
</button> </button>
<button mat-icon-button class="tb-small" </mat-form-field>
(click)="isLibraryOpen = false"
matTooltip="{{'action.close' | translate}}"
matTooltipPosition="above">
<mat-icon>chevron_left</mat-icon>
</button>
</div> </div>
</mat-toolbar> </mat-toolbar>
<div class="tb-rulechain-library-panel-group"> <div class="tb-rulechain-library-panel-group">
@ -164,6 +157,14 @@
</tb-details-panel> </tb-details-panel>
</mat-drawer> </mat-drawer>
<mat-drawer-content class="tb-rulechain-graph-content"> <mat-drawer-content class="tb-rulechain-graph-content">
<button color="primary"
mat-mini-fab
class="tb-library-node-btn"
(click)="drawer.toggle();"
matTooltip="{{ (drawer.opened ? 'rulenode.close-node-library' : 'rulenode.open-node-library') | translate }}"
matTooltipPosition="above">
<mat-icon class="tb-library-node-btn-icon" [ngClass]="{'tb-library-node-btn-icon-toggled' : drawer.opened}">chevron_right</mat-icon>
</button>
<button #versionControlButton <button #versionControlButton
*ngIf="!isImport" *ngIf="!isImport"
color="primary" color="primary"

View File

@ -54,20 +54,43 @@
z-index: 2; z-index: 2;
} }
section.tb-header-buttons.tb-library-open { .tb-library-node-btn {
width: 20px;
height: 90px;
position: absolute; position: absolute;
top: 0; top: 50%;
z-index: 1000;
opacity: 0.5;
border-radius: 0 15px 15px 0 !important;
transform: translateY(-50%);
&-icon {
transition: transform 0.4s !important;
&-toggled {
transform: rotateZ(180deg);
}
}
&:before {
width: 20px;
height: 30px;
top: -28px;
left: 0; left: 0;
z-index: 2; content: "";
pointer-events: none; pointer-events: none;
.mdc-fab.tb-btn-open-library { border-radius: 0 0 0 15px;
top: 0; box-shadow: -10px 0px 0 0px #305680;
border: none;
}
&:after {
position: absolute;
width: 20px;
height: 30px;
bottom: -28px;
left: 0; left: 0;
width: 36px; content: "";
height: 36px; pointer-events: none;
margin: 4px 0 0 4px; border-top-left-radius: 15px;
line-height: 36px; box-shadow: -10px 0px 0px 0px #305680;
opacity: .5; border: none;
} }
} }
@ -77,8 +100,7 @@
min-width: 250px; min-width: 250px;
.mat-toolbar { .mat-toolbar {
height: 48px; height: min-content;
min-height: 48px;
padding: 0; padding: 0;
.mat-toolbar-tools { .mat-toolbar-tools {

View File

@ -15,7 +15,9 @@
/// ///
import { import {
AfterViewChecked,
AfterViewInit, AfterViewInit,
ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
@ -90,6 +92,7 @@ import { MatMiniFabButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { ComponentClusteringMode } from '@shared/models/component-descriptor.models'; import { ComponentClusteringMode } from '@shared/models/component-descriptor.models';
import { MatDrawer } from '@angular/material/sidenav';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
@Component({ @Component({
@ -99,7 +102,7 @@ import Timeout = NodeJS.Timeout;
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class RuleChainPageComponent extends PageComponent export class RuleChainPageComponent extends PageComponent
implements AfterViewInit, OnInit, OnDestroy, HasDirtyFlag, ISearchableComponent { implements AfterViewInit, OnInit, OnDestroy, HasDirtyFlag, ISearchableComponent, AfterViewChecked {
get isDirty(): boolean { get isDirty(): boolean {
return this.isDirtyValue || this.isImport; return this.isDirtyValue || this.isImport;
@ -121,6 +124,8 @@ export class RuleChainPageComponent extends PageComponent
@ViewChild('ruleChainMenuTrigger', {static: true}) ruleChainMenuTrigger: MatMenuTrigger; @ViewChild('ruleChainMenuTrigger', {static: true}) ruleChainMenuTrigger: MatMenuTrigger;
@ViewChild('drawer') drawer: MatDrawer;
eventTypes = EventType; eventTypes = EventType;
debugEventTypes = DebugEventType; debugEventTypes = DebugEventType;
@ -159,7 +164,6 @@ export class RuleChainPageComponent extends PageComponent
hotKeys: Hotkey[] = []; hotKeys: Hotkey[] = [];
enableHotKeys = true; enableHotKeys = true;
isLibraryOpen = true;
ruleNodeSearch = ''; ruleNodeSearch = '';
ruleNodeTypeSearch = ''; ruleNodeTypeSearch = '';
@ -266,6 +270,7 @@ export class RuleChainPageComponent extends PageComponent
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
private renderer: Renderer2, private renderer: Renderer2,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private changeDetector: ChangeDetectorRef,
public dialog: MatDialog, public dialog: MatDialog,
public dialogService: DialogService, public dialogService: DialogService,
public fb: UntypedFormBuilder) { public fb: UntypedFormBuilder) {
@ -281,6 +286,10 @@ export class RuleChainPageComponent extends PageComponent
ngOnInit() { ngOnInit() {
} }
ngAfterViewChecked(){
this.changeDetector.detectChanges();
}
ngAfterViewInit() { ngAfterViewInit() {
fromEvent(this.ruleNodeSearchInputField.nativeElement, 'keyup') fromEvent(this.ruleNodeSearchInputField.nativeElement, 'keyup')
.pipe( .pipe(
@ -299,6 +308,14 @@ export class RuleChainPageComponent extends PageComponent
this.rxSubscription.unsubscribe(); this.rxSubscription.unsubscribe();
} }
currentRuleChainIdChanged(ruleChainId: string) {
if (this.ruleChainType === RuleChainType.CORE) {
this.router.navigateByUrl(`ruleChains/${ruleChainId}`);
} else {
this.router.navigateByUrl(`edgeManagement/ruleChains/${ruleChainId}`);
}
}
onSearchTextUpdated(searchText: string) { onSearchTextUpdated(searchText: string) {
this.ruleNodeSearch = searchText; this.ruleNodeSearch = searchText;
this.updateRuleNodesHighlight(); this.updateRuleNodesHighlight();

View File

@ -29,6 +29,14 @@
(click)="clear()"> (click)="clear()">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
<button *ngIf="selectTemplateFormGroup.get('templateName').value && !disabled && allowEdit"
type="button"
matSuffix mat-icon-button aria-label="Edit"
matTooltip="{{ 'notification.edit-notification-template' | translate }}"
matTooltipPosition="above"
(click)="editTemplate($event)">
<mat-icon class="material-icons">edit</mat-icon>
</button>
<button #createTemplateButton <button #createTemplateButton
mat-button color="primary" matSuffix mat-button color="primary" matSuffix
*ngIf="allowCreate && !selectTemplateFormGroup.get('templateName').value && !disabled" *ngIf="allowCreate && !selectTemplateFormGroup.get('templateName').value && !disabled"

View File

@ -17,7 +17,7 @@
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -66,6 +66,10 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn
@coerceBoolean() @coerceBoolean()
allowCreate = false; allowCreate = false;
@Input()
@coerceBoolean()
allowEdit = false;
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -196,18 +200,34 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn
}, 0); }, 0);
} }
editTemplate($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.notificationService.getNotificationTemplateById(this.modelValue.id).subscribe(
(template) => {
this.openNotificationTemplateDialog({template});
}
);
}
createTemplate($event: Event, button: MatButton) { createTemplate($event: Event, button: MatButton) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
button._elementRef.nativeElement.blur(); button._elementRef.nativeElement.blur();
this.openNotificationTemplateDialog({
isAdd: true,
predefinedType: this.notificationTypes
});
}
private openNotificationTemplateDialog(dialogData?: TemplateNotificationDialogData) {
this.dialog.open<TemplateNotificationDialogComponent, TemplateNotificationDialogData, this.dialog.open<TemplateNotificationDialogComponent, TemplateNotificationDialogData,
NotificationTemplate>(TemplateNotificationDialogComponent, { NotificationTemplate>(TemplateNotificationDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: dialogData
predefinedType: this.notificationTypes
}
}).afterClosed() }).afterClosed()
.subscribe((res) => { .subscribe((res) => {
if (res) { if (res) {

View File

@ -0,0 +1,27 @@
<!--
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.
-->
<mat-select fxFlex
class="tb-rule-chain-select"
[required]="required"
[disabled]="disabled"
[(ngModel)]="ruleChainId"
(ngModelChange)="ruleChainIdChanged()">
<mat-option *ngFor="let ruleChain of ruleChains$ | async" [value]="ruleChain.id.id">
{{ruleChain.name}}
</mat-option>
</mat-select>

View File

@ -0,0 +1,26 @@
/**
* 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 {
min-width: 52px;
width: 100%;
padding: 0 6px;
.tb-rule-chain-select {
display: flex;
height: 48px;
min-height: 100%;
pointer-events: all;
}
}

View File

@ -0,0 +1,109 @@
///
/// 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, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable } from 'rxjs';
import { PageLink } from '@shared/models/page/page-link';
import { map, share } from 'rxjs/operators';
import { PageData } from '@shared/models/page/page-data';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { TooltipPosition } from '@angular/material/tooltip';
import { RuleChain, RuleChainType } from '@shared/models/rule-chain.models';
import { RuleChainService } from '@core/http/rule-chain.service';
import { isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { Direction } from '@shared/models/page/sort-order';
@Component({
selector: 'tb-rule-chain-select',
templateUrl: './rule-chain-select.component.html',
styleUrls: ['./rule-chain-select.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RuleChainSelectComponent),
multi: true
}]
})
export class RuleChainSelectComponent implements ControlValueAccessor, OnInit {
@Input()
tooltipPosition: TooltipPosition = 'above';
@Input()
@coerceBoolean()
required: boolean;
@Input()
@coerceBoolean()
disabled: boolean;
@Input()
ruleChainType: RuleChainType = RuleChainType.CORE;
ruleChains$: Observable<Array<RuleChain>>;
ruleChainId: string | null;
private propagateChange = (v: any) => { };
constructor(private ruleChainService: RuleChainService) {
}
ngOnInit() {
const pageLink = new PageLink(100, 0, null, {
property: 'name',
direction: Direction.ASC
});
this.ruleChains$ = this.getRuleChains(pageLink).pipe(
map((pageData) => pageData.data),
share()
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: string | null): void {
if (isDefinedAndNotNull(value)) {
this.ruleChainId = value;
}
}
ruleChainIdChanged() {
this.updateView();
}
private updateView() {
this.propagateChange(this.ruleChainId);
}
private getRuleChains(pageLink: PageLink): Observable<PageData<RuleChain>> {
return this.ruleChainService.getRuleChains(pageLink, this.ruleChainType, {ignoreLoading: true});
}
}

View File

@ -191,6 +191,7 @@ import {
import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component'; import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component';
import { ShortNumberPipe } from '@shared/pipe/short-number.pipe'; import { ShortNumberPipe } from '@shared/pipe/short-number.pipe';
import { ToggleHeaderComponent } from '@shared/components/toggle-header.component'; import { ToggleHeaderComponent } from '@shared/components/toggle-header.component';
import { RuleChainSelectComponent } from '@shared/components/rule-chain/rule-chain-select.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService; return markedOptionsService;
@ -360,7 +361,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective, GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective, GtMdLgShowHideDirective,
ColorPickerComponent, ColorPickerComponent,
ToggleHeaderComponent ToggleHeaderComponent,
RuleChainSelectComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -586,7 +588,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective, GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective, GtMdLgShowHideDirective,
ColorPickerComponent, ColorPickerComponent,
ToggleHeaderComponent ToggleHeaderComponent,
RuleChainSelectComponent
] ]
}) })
export class SharedModule { } export class SharedModule { }

View File

@ -3349,6 +3349,7 @@
"events": "Events", "events": "Events",
"search": "Search nodes", "search": "Search nodes",
"open-node-library": "Open node library", "open-node-library": "Open node library",
"close-node-library": "Close node library",
"add": "Add rule node", "add": "Add rule node",
"name": "Name", "name": "Name",
"name-required": "Name is required.", "name-required": "Name is required.",