Commit 23121c5b5af87423c66313f072dc668c34e24faf

Authored by Igor Kulikov
1 parent 3cdeb237

Conditionally show widget header actions

... ... @@ -54,6 +54,17 @@
54 54 <tb-material-icon-select
55 55 formControlName="icon">
56 56 </tb-material-icon-select>
  57 + <mat-checkbox *ngIf="displayShowWidgetActionForm()" formControlName="useShowWidgetActionFunction" style="padding-bottom: 16px;">
  58 + {{ 'widget-config.use-show-widget-action-function' | translate }}
  59 + </mat-checkbox>
  60 + <tb-js-func *ngIf="displayShowWidgetActionForm() && widgetActionFormGroup.get('useShowWidgetActionFunction').value"
  61 + formControlName="showWidgetActionFunction"
  62 + [functionArgs]="['widgetContext', 'data']"
  63 + [globalVariables]="functionScopeVariables"
  64 + [validationArgs]="[]"
  65 + [resultType]="'boolean'"
  66 + [editorCompleter]="customActionEditorCompleter"
  67 + ></tb-js-func>
57 68 <mat-form-field class="mat-block">
58 69 <mat-label translate>widget-config.action-type</mat-label>
59 70 <mat-select required formControlName="type">
... ...
... ... @@ -38,7 +38,12 @@ import {
38 38 WidgetActionsData
39 39 } from '@home/components/widget/action/manage-widget-actions.component.models';
40 40 import { UtilsService } from '@core/services/utils.service';
41   -import { WidgetActionSource, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models';
  41 +import {
  42 + WidgetActionSource,
  43 + widgetActionSources,
  44 + WidgetActionType,
  45 + widgetActionTypeTranslationMap
  46 +} from '@shared/models/widget.models';
42 47 import { map, mergeMap, startWith, tap } from 'rxjs/operators';
43 48 import { DashboardService } from '@core/http/dashboard.service';
44 49 import { Dashboard } from '@shared/models/dashboard.models';
... ... @@ -124,17 +129,41 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
124 129 this.fb.control(this.action.name, [this.validateActionName(), Validators.required]));
125 130 this.widgetActionFormGroup.addControl('icon',
126 131 this.fb.control(this.action.icon, [Validators.required]));
  132 + this.widgetActionFormGroup.addControl('useShowWidgetActionFunction',
  133 + this.fb.control(this.action.useShowWidgetActionFunction, []));
  134 + this.widgetActionFormGroup.addControl('showWidgetActionFunction',
  135 + this.fb.control(this.action.showWidgetActionFunction || 'return true;', []));
127 136 this.widgetActionFormGroup.addControl('type',
128 137 this.fb.control(this.action.type, [Validators.required]));
  138 + this.updateShowWidgetActionForm();
129 139 this.updateActionTypeFormGroup(this.action.type, this.action);
130 140 this.widgetActionFormGroup.get('type').valueChanges.subscribe((type: WidgetActionType) => {
131 141 this.updateActionTypeFormGroup(type);
132 142 });
133 143 this.widgetActionFormGroup.get('actionSourceId').valueChanges.subscribe(() => {
134 144 this.widgetActionFormGroup.get('name').updateValueAndValidity();
  145 + this.updateShowWidgetActionForm();
  146 + });
  147 + this.widgetActionFormGroup.get('useShowWidgetActionFunction').valueChanges.subscribe(() => {
  148 + this.updateShowWidgetActionForm();
135 149 });
136 150 }
137 151
  152 + displayShowWidgetActionForm(): boolean {
  153 + return this.widgetActionFormGroup.get('actionSourceId').value === widgetActionSources.headerButton.value;
  154 + }
  155 +
  156 + private updateShowWidgetActionForm() {
  157 + const actionSourceId = this.widgetActionFormGroup.get('actionSourceId').value;
  158 + const useShowWidgetActionFunction = this.widgetActionFormGroup.get('useShowWidgetActionFunction').value;
  159 + if (actionSourceId === widgetActionSources.headerButton.value && useShowWidgetActionFunction) {
  160 + this.widgetActionFormGroup.get('showWidgetActionFunction').setValidators([Validators.required]);
  161 + } else {
  162 + this.widgetActionFormGroup.get('showWidgetActionFunction').clearValidators();
  163 + }
  164 + this.widgetActionFormGroup.get('showWidgetActionFunction').updateValueAndValidity();
  165 + }
  166 +
138 167 private updateActionTypeFormGroup(type?: WidgetActionType, action?: WidgetActionDescriptorInfo) {
139 168 this.actionTypeFormGroup = this.fb.group({});
140 169 if (type) {
... ...
... ... @@ -330,7 +330,7 @@ export function parseData(input: DatasourceData[]): FormattedData[] {
330 330 deviceType: null
331 331 };
332 332 entityArray.filter(el => el.data.length).forEach(el => {
333   - const indexDate = el.data.length ? el.data.length - 1 : 0;
  333 + const indexDate = el.data.length - 1;
334 334 if (!obj.hasOwnProperty(el.dataKey.label) || el.data[indexDate][1] !== '') {
335 335 obj[el.dataKey.label] = el.data[indexDate][1];
336 336 obj[el.dataKey.label + '|ts'] = el.data[indexDate][0];
... ...
... ... @@ -55,9 +55,17 @@ import { AppState } from '@core/core.state';
55 55 import { WidgetService } from '@core/http/widget.service';
56 56 import { UtilsService } from '@core/services/utils.service';
57 57 import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs';
58   -import { deepClone, insertVariable, isDefined, objToBase64, objToBase64URI, validateEntityId } from '@core/utils';
59 58 import {
60   - IDynamicWidgetComponent,
  59 + deepClone,
  60 + insertVariable,
  61 + isDefined,
  62 + isNotEmptyStr,
  63 + objToBase64,
  64 + objToBase64URI,
  65 + validateEntityId
  66 +} from '@core/utils';
  67 +import {
  68 + IDynamicWidgetComponent, ShowWidgetHeaderActionFunction,
61 69 WidgetContext,
62 70 WidgetHeaderAction,
63 71 WidgetInfo,
... ... @@ -290,12 +298,25 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
290 298
291 299 this.widgetContext.customHeaderActions = [];
292 300 const headerActionsDescriptors = this.getActionDescriptors(widgetActionSources.headerButton.value);
293   - headerActionsDescriptors.forEach((descriptor) => {
  301 + headerActionsDescriptors.forEach((descriptor) =>
  302 + {
  303 + let useShowWidgetHeaderActionFunction = descriptor.useShowWidgetActionFunction || false;
  304 + let showWidgetHeaderActionFunction: ShowWidgetHeaderActionFunction = null;
  305 + if (useShowWidgetHeaderActionFunction && isNotEmptyStr(descriptor.showWidgetActionFunction)) {
  306 + try {
  307 + showWidgetHeaderActionFunction =
  308 + new Function('widgetContext', 'data', descriptor.showWidgetActionFunction) as ShowWidgetHeaderActionFunction;
  309 + } catch (e) {
  310 + useShowWidgetHeaderActionFunction = false;
  311 + }
  312 + }
294 313 const headerAction: WidgetHeaderAction = {
295 314 name: descriptor.name,
296 315 displayName: descriptor.displayName,
297 316 icon: descriptor.icon,
298 317 descriptor,
  318 + useShowWidgetHeaderActionFunction,
  319 + showWidgetHeaderActionFunction,
299 320 onAction: $event => {
300 321 const entityInfo = this.getActiveEntityInfo();
301 322 const entityId = entityInfo ? entityInfo.entityId : null;
... ... @@ -507,6 +528,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
507 528 this.widgetInstanceInited = true;
508 529 if (this.dataUpdatePending) {
509 530 this.widgetTypeInstance.onDataUpdated();
  531 + setTimeout(() => {
  532 + this.dashboardWidget.updateCustomHeaderActions(true);
  533 + }, 0);
510 534 this.dataUpdatePending = false;
511 535 }
512 536 if (this.pendingMessage) {
... ... @@ -844,6 +868,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
844 868 if (this.displayWidgetInstance()) {
845 869 if (this.widgetInstanceInited) {
846 870 this.widgetTypeInstance.onDataUpdated();
  871 + setTimeout(() => {
  872 + this.dashboardWidget.updateCustomHeaderActions(true);
  873 + }, 0);
847 874 } else {
848 875 this.dataUpdatePending = true;
849 876 }
... ...
... ... @@ -25,6 +25,8 @@ import { IterableDiffer, KeyValueDiffer } from '@angular/core';
25 25 import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
26 26 import { enumerable } from '@shared/decorators/enumerable';
27 27 import { UtilsService } from '@core/services/utils.service';
  28 +import { FormattedData } from '@home/components/widget/lib/maps/map-models';
  29 +import { parseData } from '@home/components/widget/lib/maps/common-maps-utils';
28 30
29 31 export interface WidgetsData {
30 32 widgets: Array<Widget>;
... ... @@ -438,13 +440,45 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
438 440
439 441 this.showWidgetActions = !this.widgetContext.hideTitlePanel;
440 442
441   - this.customHeaderActions = this.widgetContext.customHeaderActions ? this.widgetContext.customHeaderActions : [];
  443 + this.updateCustomHeaderActions();
442 444 this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : [];
443 445 if (detectChanges) {
444 446 this.widgetContext.detectContainerChanges();
445 447 }
446 448 }
447 449
  450 + updateCustomHeaderActions(detectChanges = false) {
  451 + let customHeaderActions: Array<WidgetHeaderAction>;
  452 + if (this.widgetContext.customHeaderActions) {
  453 + let data: FormattedData[] = [];
  454 + if (this.widgetContext.customHeaderActions.some(action => action.useShowWidgetHeaderActionFunction)) {
  455 + data = parseData(this.widgetContext.data || []);
  456 + }
  457 + customHeaderActions = this.widgetContext.customHeaderActions.filter(action => this.filterCustomHeaderAction(action, data));
  458 + } else {
  459 + customHeaderActions = [];
  460 + }
  461 + if (!isEqual(this.customHeaderActions, customHeaderActions)) {
  462 + this.customHeaderActions = customHeaderActions;
  463 + if (detectChanges) {
  464 + this.widgetContext.detectContainerChanges();
  465 + }
  466 + }
  467 + }
  468 +
  469 + private filterCustomHeaderAction(action: WidgetHeaderAction, data: FormattedData[]): boolean {
  470 + if (action.useShowWidgetHeaderActionFunction) {
  471 + try {
  472 + return action.showWidgetHeaderActionFunction(this.widgetContext, data);
  473 + } catch (e) {
  474 + console.warn('Failed to execute showWidgetHeaderActionFunction', e);
  475 + return false;
  476 + }
  477 + } else {
  478 + return true;
  479 + }
  480 + }
  481 +
448 482 @enumerable(true)
449 483 get x(): number {
450 484 let res;
... ...
... ... @@ -79,6 +79,7 @@ import { SortOrder } from '@shared/models/page/sort-order';
79 79 import { DomSanitizer } from '@angular/platform-browser';
80 80 import { Router } from '@angular/router';
81 81 import { catchError, map, mergeMap, switchMap } from 'rxjs/operators';
  82 +import { FormattedData } from '@home/components/widget/lib/maps/map-models';
82 83
83 84 export interface IWidgetAction {
84 85 name: string;
... ... @@ -86,9 +87,13 @@ export interface IWidgetAction {
86 87 onAction: ($event: Event) => void;
87 88 }
88 89
  90 +export type ShowWidgetHeaderActionFunction = (ctx: WidgetContext, data: FormattedData[]) => boolean;
  91 +
89 92 export interface WidgetHeaderAction extends IWidgetAction {
90 93 displayName: string;
91 94 descriptor: WidgetActionDescriptor;
  95 + useShowWidgetHeaderActionFunction: boolean;
  96 + showWidgetHeaderActionFunction: ShowWidgetHeaderActionFunction;
92 97 }
93 98
94 99 export interface WidgetAction extends IWidgetAction {
... ...
... ... @@ -390,6 +390,63 @@ export const widgetContextCompletions: TbEditorCompletions = {
390 390 meta: 'property',
391 391 type: 'number'
392 392 },
  393 + currentUser: {
  394 + description: 'Current user object.',
  395 + meta: 'property',
  396 + type: '<a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/user.model.ts#L45">AuthUser</a>',
  397 + children: {
  398 + sub: {
  399 + description: 'User subject (email).',
  400 + meta: 'property',
  401 + type: 'string'
  402 + },
  403 + scopes: {
  404 + description: 'User security scopes.',
  405 + meta: 'property',
  406 + type: 'Array<string>'
  407 + },
  408 + userId: {
  409 + description: 'User id.',
  410 + meta: 'property',
  411 + type: 'string'
  412 + },
  413 + firstName: {
  414 + description: 'User first name.',
  415 + meta: 'property',
  416 + type: 'string'
  417 + },
  418 + lastName: {
  419 + description: 'User last name.',
  420 + meta: 'property',
  421 + type: 'string'
  422 + },
  423 + enabled: {
  424 + description: 'Whether is user enabled.',
  425 + meta: 'property',
  426 + type: 'boolean'
  427 + },
  428 + tenantId: {
  429 + description: 'Tenant id of the user.',
  430 + meta: 'property',
  431 + type: 'string'
  432 + },
  433 + customerId: {
  434 + description: 'Customer id of the user (available when user belongs to specific customer).',
  435 + meta: 'property',
  436 + type: 'string'
  437 + },
  438 + isPublic: {
  439 + description: 'Special flag indicating public user.',
  440 + meta: 'property',
  441 + type: 'boolean'
  442 + },
  443 + authority: {
  444 + description: 'User authority. Possible values: SYS_ADMIN, TENANT_ADMIN, CUSTOMER_USER',
  445 + meta: 'property',
  446 + type: '<a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/authority.enum.ts#L17">Authority</a>'
  447 + }
  448 + }
  449 + },
393 450 hideTitlePanel: {
394 451 description: 'Manages visibility of widget title panel. Useful for widget with custom title panels or different states. <b>updateWidgetParams()</b> function must be called after this property change.',
395 452 meta: 'property',
... ...
... ... @@ -462,6 +462,8 @@ export interface WidgetActionDescriptor extends CustomActionDescriptor {
462 462 setEntityId?: boolean;
463 463 stateEntityParamName?: string;
464 464 mobileAction?: WidgetMobileActionDescriptor;
  465 + useShowWidgetActionFunction?: boolean;
  466 + showWidgetActionFunction?: string;
465 467 }
466 468
467 469 export interface WidgetComparisonSettings {
... ...
... ... @@ -3038,6 +3038,7 @@
3038 3038 "action-name-required": "Action name is required.",
3039 3039 "action-name-not-unique": "Another action with the same name already exists.<br/>Action name should be unique within the same action source.",
3040 3040 "action-icon": "Icon",
  3041 + "use-show-widget-action-function": "Use show widget action function",
3041 3042 "action-type": "Type",
3042 3043 "action-type-required": "Action type is required.",
3043 3044 "edit-action": "Edit action",
... ...