thingsboard / thingsboard

Open-source IoT Platform - Device management, data collection, processing and visualization.
https://thingsboard.io
Apache License 2.0
17.4k stars 5.14k forks source link

User creation from a dashboard. #5976

Closed lucasKapf closed 2 years ago

lucasKapf commented 2 years ago

Hello,

I have a dashboard which displays all my customers. I would like to know if it is possible to create Users for my customers from this dashboard ?

Best regards,

Lucas

mde2017 commented 2 years ago

Hi @lucasKapf,

I did something similiar with creating Customers from a Dashboard using Custom Widgets (with HTML Template) . In your case you could use the user.service to create a new User from within a Dashboard: https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/core/http/user.service.ts

 public saveUser(user: User, sendActivationMail: boolean = false,
                  config?: RequestConfig): Observable<User> {
    let url = '/api/user';
    url += '?sendActivationMail=' + sendActivationMail;
    return this.http.post<User>(url, user, defaultHttpOptionsFromConfig(config));
  }

I hope this helps

lucasKapf commented 2 years ago

Hello @mde2017, thank you for your quick answer, this helps a lot. I will check it. Just one more question, do you know how to build a User object ?

lucasKapf commented 2 years ago

Nevermind I have found it, it is declared here https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/user.model.ts

ashvayka commented 2 years ago

The suggested method by @mde2017 is working but far from perfect because it is using the raw http post instead of service methods. I am sharing the source code of dialog to create a user for ThingsBoard PE. It is specific to PE cause it also adds the user to specific group (using "entityGroupService"). @vvlladd28 please share the sample code for CE as well.

HTML:

<form #addEntityForm="ngForm" [formGroup]="addUserFormGroup"
      (ngSubmit)="save()" class="add-entity-form" style="width: 600px;">
  <mat-toolbar fxLayout="row" color="primary">
    <h2>Add Smart Retail Administrator</h2>
    <span fxFlex></span>
    <button mat-icon-button (click)="cancel()" type="button">
      <mat-icon class="material-icons">close</mat-icon>
    </button>
  </mat-toolbar>
  <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  </mat-progress-bar>
  <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  <div mat-dialog-content fxLayout="column">
    <mat-form-field fxFlex class="mat-block">
        <mat-label>Email</mat-label>
        <input matInput formControlName="email" type="email" required>
        <mat-error *ngIf="addUserFormGroup.get('email').hasError('required')">
            Email is required
        </mat-error>
        <mat-error *ngIf="addUserFormGroup.get('email').hasError('pattern')">
            Invalid value format
        </mat-error>
    </mat-form-field>
    <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
        <mat-form-field fxFlex class="mat-block">
            <mat-label>First Name</mat-label>
            <input matInput formControlName="firstName">
        </mat-form-field>
        <mat-form-field fxFlex class="mat-block">
            <mat-label>Last Name</mat-label>
            <input matInput formControlName="lastName" >
        </mat-form-field>
    </div>
    <mat-form-field fxFlex class="mat-block">
        <mat-label>Activation method</mat-label>
        <mat-select formControlName="userActivationMethod">
            <mat-option *ngFor="let method of activationMethods" [value]="method.value">
                {{ method.name }}
            </mat-option>
        </mat-select>
    </mat-form-field>
  </div>
  <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
    <button mat-button color="primary"
            type="button"
            [disabled]="(isLoading$ | async)"
            (click)="cancel()" cdkFocusInitial>
      Cancel
    </button>
    <button mat-button mat-raised-button color="primary"
            type="submit"
            [disabled]="(isLoading$ | async) || addUserFormGroup.invalid || !addUserFormGroup.dirty">
      Add user
    </button>
  </div>
</form>

JS function:

let $injector = widgetContext.$scope.$injector;
let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
let userService = $injector.get(widgetContext.servicesMap.get('userService'));
let entityGroupService = $injector.get(widgetContext.servicesMap.get('entityGroupService'));
let dashboardService = $injector.get(widgetContext.servicesMap.get('dashboardService'));

openAddUserDialog();

function openAddUserDialog() {
  customDialog.customDialog(htmlTemplate, AddUserDialogController).subscribe();
}

function AddUserDialogController(instance) {
  let vm = instance;

  vm.activationMethods = [
        {
            value: 'displayActivationLink',
            name: 'Display activation link'
        },
        {
            value: 'sendActivationMail',
            name: 'Send activation email'
        }
  ];

  vm.addUserFormGroup = vm.fb.group({
    email: ['', [vm.validators.required, vm.validators.pattern(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\_\-0-9]+\.)+[a-zA-Z]{2,}))$/)]],
    firstName: [null],
    lastName: [null],
    userActivationMethod: ['displayActivationLink']
  });

  vm.cancel = function () {
    vm.dialogRef.close(null);
  };

  vm.save = function () {
    var customerId;
    if (widgetContext.currentUser.authority === 'TENANT_ADMIN') {
        customerId = widgetContext.stateController.getStateParams().entityId;
    } else {
        customerId = { id: widgetContext.currentUser.customerId, entityType: 'CUSTOMER'};
    }
    vm.addUserFormGroup.markAsPristine();

    const formValues = vm.addUserFormGroup.value;
    let user = {
      email: formValues.email,
      firstName: formValues.firstName,
      lastName: formValues.lastName,
      authority: 'CUSTOMER_USER',
      customerId: customerId
    };
    const sendActivationMail = (formValues.userActivationMethod === 'sendActivationMail');

    widgetContext.rxjs.forkJoin([
        getTargetUserGroup(customerId), 
        getDashboardByName('Smart Supermarket Administration')
    ]).pipe(
        widgetContext.rxjs.switchMap((data) => {
            var userGroup = data[0];
            var defaultDashboard = data[1];
            if (defaultDashboard) {
                user.additionalInfo = {
                    defaultDashboardId: defaultDashboard.id.id,
                    defaultDashboardFullscreen: true
                };
            }
            return saveUserObservable(userGroup, user, sendActivationMail);
        })
    ).subscribe((user) => {
        widgetContext.updateAliases();
        if (formValues.userActivationMethod === 'displayActivationLink') {
            userService.getActivationLink(user.id.id).subscribe(
                (activationLink) => {
                    displayActivationLink(activationLink).subscribe(
                        () => {
                            vm.dialogRef.close(null);
                        }
                    );
                }
            );
        } else {
            vm.dialogRef.close(null);
        }
    });
  };

  function saveUserObservable(userGroup, user, sendActivationMail) {
      return userService.saveUser(user, sendActivationMail, userGroup.id.id);
  }

  function getTargetUserGroup(customerId) {
      return entityGroupService.getEntityGroupsByOwnerId(customerId.entityType, customerId.id, 'USER').pipe(
          widgetContext.rxjs.switchMap((groups) => {
              return getOrCreateUserGroup(groups, 'Smart Retail Administrators', customerId);
          })
      );
  }

  function getOrCreateUserGroup(groups, groupName, customerId) {
      var usersGroup = groups.find(group => group.name === groupName);
      if (usersGroup) {
          return widgetContext.rxjs.of(usersGroup);
      } else {
          usersGroup = {
              type: 'USER',
              name: groupName,
              ownerId: customerId
          };
          return entityGroupService.saveEntityGroup(usersGroup);
      }
  }

  function getDashboardByName(dashboardName) {
      var dashboardsPageLink = widgetContext.pageLink(10, 0, dashboardName);
      return dashboardService.getUserDashboards(null, null, dashboardsPageLink, {ignoreLoading: true}).pipe(
          widgetContext.rxjs.map((data) => {
            if (data.data.length) {
                return data.data.find((dashboard) => dashboard.name === dashboardName);
            } else {
                return null;
            }
          })
      );
  }

    function displayActivationLink(activationLink) {
        const template = '<form style="min-width: 400px;">\n' +
            '  <mat-toolbar color="primary">\n' +
            '    <h2 translate>user.activation-link</h2>\n' +
            '    <span fxFlex></span>\n' +
            '    <button mat-button mat-icon-button\n' +
            '            (click)="close()"\n' +
            '            type="button">\n' +
            '      <mat-icon class="material-icons">close</mat-icon>\n' +
            '    </button>\n' +
            '  </mat-toolbar>\n' +
            '  <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">\n' +
            '  </mat-progress-bar>\n' +
            '  <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>\n' +
            '  <div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent">\n' +
            '    <div class="mat-content" fxLayout="column">\n' +
            '      <span [innerHTML]="\'user.activation-link-text\' | translate: {activationLink: activationLink}"></span>\n' +
            '      <div fxLayout="row" fxLayoutAlign="start center">\n' +
            '        <pre class="tb-highlight" fxFlex><code>{{ activationLink }}</code></pre>\n' +
            '        <button mat-icon-button\n' +
            '                color="primary"\n' +
            '                ngxClipboard\n' +
            '                cbContent="{{ activationLink }}"\n' +
            '                (cbOnSuccess)="onActivationLinkCopied()"\n' +
            '                matTooltip="{{ \'user.copy-activation-link\' | translate }}"\n' +
            '                matTooltipPosition="above">\n' +
            '          <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>\n' +
            '        </button>\n' +
            '      </div>\n' +
            '    </div>\n' +
            '  </div>\n' +
            '  <div mat-dialog-actions fxLayoutAlign="end center">\n' +
            '    <button mat-button color="primary"\n' +
            '            type="button"\n' +
            '            cdkFocusInitial\n' +
            '            [disabled]="(isLoading$ | async)"\n' +
            '            (click)="close()">\n' +
            '      {{ \'action.ok\' | translate }}\n' +
            '    </button>\n' +
            '  </div>\n' +
            '</form>';
        return customDialog.customDialog(template, ActivationLinkDialogController, {activationLink: activationLink});
    }

    function ActivationLinkDialogController(instance) {
        var vm = instance;

        vm.activationLink = instance.data.activationLink;

        vm.onActivationLinkCopied = onActivationLinkCopied;
        vm.close = close;

        function onActivationLinkCopied(){
            widgetContext.showSuccessToast(translate.instant('user.activation-link-copied-message'), 1000, 'bottom', 'left', 'activationLinkDialogContent');
        }

        function close() {
            vm.dialogRef.close(null);
        }
    }

}
lucasKapf commented 2 years ago

@ashvayka Thank you very much for the code, I will do it this way. I'm using the Professional Edition so it should work.

lucasKapf commented 2 years ago

@ashvayka Do you know how to suggest existing dashboard, like it is the case when you edit a user dashboard from the Customer Hierarchy ? I tried this but it doesn't seem to work:

<tb-entity-subtype-autocomplete fxFlex="50"
                    formControlName="dashboardName"
                    [required]="true"
                    [entityType]="'DASHBOARD'">
                </tb-entity-subtype-autocomplete>

Or maybe it is not possible to do that from a dashboard ?

vvlladd28 commented 2 years ago

Hi @lucasKapf, tb-entity-subtype-autocomplete is for the selection entity subtype. You need to use component tb-entiti-autocomplete for your use case.

lucasKapf commented 2 years ago

Hi @vvlladd28 Thank you for your answer, I will look into it.

PhilipRai commented 1 week ago

Regarding the CustomerGroups on this nice little script, Is there a way to "transfer". the current users assigned groups to the new user? I can only get the group that's mentioned in the script to be transferred.

function getTargetUserGroup(customerId) { return entityGroupService.getEntityGroupsByOwnerId(customerId.entityType, customerId.id, 'USER').pipe( widgetContext.rxjs.switchMap((groups) => { return getOrCreateUserGroup(groups, 'Smart Retail Administrators', customerId); }) ); }

function getOrCreateUserGroup(groups, groupName, customerId) { var usersGroup = groups.find(group => group.name === groupName); if (usersGroup) { return widgetContext.rxjs.of(usersGroup); } else { usersGroup = { type: 'USER', name: groupName, ownerId: customerId }; return entityGroupService.saveEntityGroup(usersGroup); }