Commit 52e6e76ac6c46cbead43f17b6c53b7ebc967dba6

Authored by Igor Kulikov
1 parent b0126a9d

Oauth2 - set provider name to created user additionalInfo. UI: Improve device profile alarm rules.

Showing 28 changed files with 165 additions and 175 deletions
@@ -31,6 +31,8 @@ import org.thingsboard.server.common.data.id.CustomerId; @@ -31,6 +31,8 @@ import org.thingsboard.server.common.data.id.CustomerId;
31 import org.thingsboard.server.common.data.id.DashboardId; 31 import org.thingsboard.server.common.data.id.DashboardId;
32 import org.thingsboard.server.common.data.id.IdBased; 32 import org.thingsboard.server.common.data.id.IdBased;
33 import org.thingsboard.server.common.data.id.TenantId; 33 import org.thingsboard.server.common.data.id.TenantId;
  34 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
  35 +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
34 import org.thingsboard.server.common.data.page.PageData; 36 import org.thingsboard.server.common.data.page.PageData;
35 import org.thingsboard.server.common.data.page.PageLink; 37 import org.thingsboard.server.common.data.page.PageLink;
36 import org.thingsboard.server.common.data.security.Authority; 38 import org.thingsboard.server.common.data.security.Authority;
@@ -76,12 +78,15 @@ public abstract class AbstractOAuth2ClientMapper { @@ -76,12 +78,15 @@ public abstract class AbstractOAuth2ClientMapper {
76 78
77 private final Lock userCreationLock = new ReentrantLock(); 79 private final Lock userCreationLock = new ReentrantLock();
78 80
79 - protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, boolean allowUserCreation, boolean activateUser) { 81 + protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, OAuth2ClientRegistrationInfo clientRegistration) {
  82 +
  83 + OAuth2MapperConfig config = clientRegistration.getMapperConfig();
  84 +
80 UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail()); 85 UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail());
81 86
82 User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail()); 87 User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail());
83 88
84 - if (user == null && !allowUserCreation) { 89 + if (user == null && !config.isAllowUserCreation()) {
85 throw new UsernameNotFoundException("User not found: " + oauth2User.getEmail()); 90 throw new UsernameNotFoundException("User not found: " + oauth2User.getEmail());
86 } 91 }
87 92
@@ -106,21 +111,28 @@ public abstract class AbstractOAuth2ClientMapper { @@ -106,21 +111,28 @@ public abstract class AbstractOAuth2ClientMapper {
106 user.setFirstName(oauth2User.getFirstName()); 111 user.setFirstName(oauth2User.getFirstName());
107 user.setLastName(oauth2User.getLastName()); 112 user.setLastName(oauth2User.getLastName());
108 113
  114 + ObjectNode additionalInfo = objectMapper.createObjectNode();
  115 +
109 if (!StringUtils.isEmpty(oauth2User.getDefaultDashboardName())) { 116 if (!StringUtils.isEmpty(oauth2User.getDefaultDashboardName())) {
110 Optional<DashboardId> dashboardIdOpt = 117 Optional<DashboardId> dashboardIdOpt =
111 user.getAuthority() == Authority.TENANT_ADMIN ? 118 user.getAuthority() == Authority.TENANT_ADMIN ?
112 getDashboardId(tenantId, oauth2User.getDefaultDashboardName()) 119 getDashboardId(tenantId, oauth2User.getDefaultDashboardName())
113 : getDashboardId(tenantId, customerId, oauth2User.getDefaultDashboardName()); 120 : getDashboardId(tenantId, customerId, oauth2User.getDefaultDashboardName());
114 if (dashboardIdOpt.isPresent()) { 121 if (dashboardIdOpt.isPresent()) {
115 - ObjectNode additionalInfo = objectMapper.createObjectNode();  
116 additionalInfo.put("defaultDashboardFullscreen", oauth2User.isAlwaysFullScreen()); 122 additionalInfo.put("defaultDashboardFullscreen", oauth2User.isAlwaysFullScreen());
117 additionalInfo.put("defaultDashboardId", dashboardIdOpt.get().getId().toString()); 123 additionalInfo.put("defaultDashboardId", dashboardIdOpt.get().getId().toString());
118 - user.setAdditionalInfo(additionalInfo);  
119 } 124 }
120 } 125 }
121 126
  127 + if (clientRegistration.getAdditionalInfo() != null &&
  128 + clientRegistration.getAdditionalInfo().has("providerName")) {
  129 + additionalInfo.put("authProviderName", clientRegistration.getAdditionalInfo().get("providerName").asText());
  130 + }
  131 +
  132 + user.setAdditionalInfo(additionalInfo);
  133 +
122 user = userService.saveUser(user); 134 user = userService.saveUser(user);
123 - if (activateUser) { 135 + if (config.isActivateUser()) {
124 UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()); 136 UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId());
125 userService.activateUserCredentials(user.getTenantId(), userCredentials.getActivateToken(), passwordEncoder.encode("")); 137 userService.activateUserCredentials(user.getTenantId(), userCredentials.getActivateToken(), passwordEncoder.encode(""));
126 } 138 }
@@ -18,6 +18,7 @@ package org.thingsboard.server.service.security.auth.oauth2; @@ -18,6 +18,7 @@ package org.thingsboard.server.service.security.auth.oauth2;
18 import lombok.extern.slf4j.Slf4j; 18 import lombok.extern.slf4j.Slf4j;
19 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 19 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
20 import org.springframework.stereotype.Service; 20 import org.springframework.stereotype.Service;
  21 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
21 import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; 22 import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
22 import org.thingsboard.server.dao.oauth2.OAuth2User; 23 import org.thingsboard.server.dao.oauth2.OAuth2User;
23 import org.thingsboard.server.service.security.model.SecurityUser; 24 import org.thingsboard.server.service.security.model.SecurityUser;
@@ -29,11 +30,12 @@ import java.util.Map; @@ -29,11 +30,12 @@ import java.util.Map;
29 public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { 30 public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
30 31
31 @Override 32 @Override
32 - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) { 33 + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) {
  34 + OAuth2MapperConfig config = clientRegistration.getMapperConfig();
33 Map<String, Object> attributes = token.getPrincipal().getAttributes(); 35 Map<String, Object> attributes = token.getPrincipal().getAttributes();
34 String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); 36 String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
35 OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); 37 OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config);
36 38
37 - return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser()); 39 + return getOrCreateSecurityUserFromOAuth2User(oauth2User, clientRegistration);
38 } 40 }
39 } 41 }
@@ -23,6 +23,7 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic @@ -23,6 +23,7 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic
23 import org.springframework.stereotype.Service; 23 import org.springframework.stereotype.Service;
24 import org.springframework.util.StringUtils; 24 import org.springframework.util.StringUtils;
25 import org.springframework.web.client.RestTemplate; 25 import org.springframework.web.client.RestTemplate;
  26 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
26 import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; 27 import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig;
27 import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; 28 import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
28 import org.thingsboard.server.dao.oauth2.OAuth2User; 29 import org.thingsboard.server.dao.oauth2.OAuth2User;
@@ -38,9 +39,10 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme @@ -38,9 +39,10 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
38 private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); 39 private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
39 40
40 @Override 41 @Override
41 - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) { 42 + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) {
  43 + OAuth2MapperConfig config = clientRegistration.getMapperConfig();
42 OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom()); 44 OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom());
43 - return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser()); 45 + return getOrCreateSecurityUserFromOAuth2User(oauth2User, clientRegistration);
44 } 46 }
45 47
46 private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2CustomMapperConfig custom) { 48 private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2CustomMapperConfig custom) {
@@ -23,6 +23,7 @@ import org.springframework.boot.web.client.RestTemplateBuilder; @@ -23,6 +23,7 @@ import org.springframework.boot.web.client.RestTemplateBuilder;
23 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 23 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
24 import org.springframework.stereotype.Service; 24 import org.springframework.stereotype.Service;
25 import org.springframework.web.client.RestTemplate; 25 import org.springframework.web.client.RestTemplate;
  26 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
26 import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; 27 import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
27 import org.thingsboard.server.dao.oauth2.OAuth2Configuration; 28 import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
28 import org.thingsboard.server.dao.oauth2.OAuth2User; 29 import org.thingsboard.server.dao.oauth2.OAuth2User;
@@ -45,12 +46,13 @@ public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme @@ -45,12 +46,13 @@ public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
45 private OAuth2Configuration oAuth2Configuration; 46 private OAuth2Configuration oAuth2Configuration;
46 47
47 @Override 48 @Override
48 - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) { 49 + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) {
  50 + OAuth2MapperConfig config = clientRegistration.getMapperConfig();
49 Map<String, String> githubMapperConfig = oAuth2Configuration.getGithubMapper(); 51 Map<String, String> githubMapperConfig = oAuth2Configuration.getGithubMapper();
50 String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken); 52 String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken);
51 Map<String, Object> attributes = token.getPrincipal().getAttributes(); 53 Map<String, Object> attributes = token.getPrincipal().getAttributes();
52 OAuth2User oAuth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); 54 OAuth2User oAuth2User = BasicMapperUtils.getOAuth2User(email, attributes, config);
53 - return getOrCreateSecurityUserFromOAuth2User(oAuth2User, config.isAllowUserCreation(), config.isActivateUser()); 55 + return getOrCreateSecurityUserFromOAuth2User(oAuth2User, clientRegistration);
54 } 56 }
55 57
56 private synchronized String getEmail(String emailUrl, String oauth2Token) { 58 private synchronized String getEmail(String emailUrl, String oauth2Token) {
@@ -16,9 +16,9 @@ @@ -16,9 +16,9 @@
16 package org.thingsboard.server.service.security.auth.oauth2; 16 package org.thingsboard.server.service.security.auth.oauth2;
17 17
18 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 18 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
19 -import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; 19 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
20 import org.thingsboard.server.service.security.model.SecurityUser; 20 import org.thingsboard.server.service.security.model.SecurityUser;
21 21
22 public interface OAuth2ClientMapper { 22 public interface OAuth2ClientMapper {
23 - SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config); 23 + SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration);
24 } 24 }
@@ -74,7 +74,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS @@ -74,7 +74,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
74 token.getPrincipal().getName()); 74 token.getPrincipal().getName());
75 OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(clientRegistration.getMapperConfig().getType()); 75 OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(clientRegistration.getMapperConfig().getType());
76 SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), 76 SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(),
77 - clientRegistration.getMapperConfig()); 77 + clientRegistration);
78 78
79 JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); 79 JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
80 JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); 80 JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
@@ -85,4 +85,4 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS @@ -85,4 +85,4 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
85 URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8.toString())); 85 URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8.toString()));
86 } 86 }
87 } 87 }
88 -}  
  88 +}
@@ -33,11 +33,6 @@ @@ -33,11 +33,6 @@
33 <tb-anchor #entityDetailsForm></tb-anchor> 33 <tb-anchor #entityDetailsForm></tb-anchor>
34 </div> 34 </div>
35 <div mat-dialog-actions fxLayoutAlign="end center"> 35 <div mat-dialog-actions fxLayoutAlign="end center">
36 - <button mat-raised-button color="primary"  
37 - type="submit"  
38 - [disabled]="(isLoading$ | async) || detailsForm?.invalid || !detailsForm?.dirty">  
39 - {{ 'action.add' | translate }}  
40 - </button>  
41 <button mat-button color="primary" 36 <button mat-button color="primary"
42 type="button" 37 type="button"
43 cdkFocusInitial 38 cdkFocusInitial
@@ -45,5 +40,10 @@ @@ -45,5 +40,10 @@
45 (click)="cancel()"> 40 (click)="cancel()">
46 {{ 'action.cancel' | translate }} 41 {{ 'action.cancel' | translate }}
47 </button> 42 </button>
  43 + <button mat-raised-button color="primary"
  44 + type="submit"
  45 + [disabled]="(isLoading$ | async) || detailsForm?.invalid || !detailsForm?.dirty">
  46 + {{ 'action.add' | translate }}
  47 + </button>
48 </div> 48 </div>
49 </form> 49 </form>
@@ -15,5 +15,5 @@ @@ -15,5 +15,5 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div class="tb-filter-text" [ngClass]="{disabled: disabled, required: requiredClass}" 18 +<div class="tb-filter-text" [ngClass]="{disabled: disabled, required: requiredClass, nowrap: nowrap}"
19 [innerHTML]="filterText"></div> 19 [innerHTML]="filterText"></div>
@@ -21,6 +21,11 @@ @@ -21,6 +21,11 @@
21 } 21 }
22 &.required { 22 &.required {
23 color: #f44336; 23 color: #f44336;
  24 + padding: 0 4px;
  25 + }
  26 + &.nowrap {
  27 + white-space: nowrap;
  28 + overflow: hidden;
24 } 29 }
25 } 30 }
26 } 31 }
@@ -54,6 +54,9 @@ export class FilterTextComponent implements ControlValueAccessor, OnInit { @@ -54,6 +54,9 @@ export class FilterTextComponent implements ControlValueAccessor, OnInit {
54 @Input() 54 @Input()
55 addFilterPrompt = this.translate.instant('filter.add-filter-prompt'); 55 addFilterPrompt = this.translate.instant('filter.add-filter-prompt');
56 56
  57 + @Input()
  58 + nowrap = false;
  59 +
57 requiredClass = false; 60 requiredClass = false;
58 61
59 private filterText: string; 62 private filterText: string;
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div style="min-width: 1000px;"> 18 +<div>
19 <mat-toolbar color="primary"> 19 <mat-toolbar color="primary">
20 <h2 translate>device-profile.add</h2> 20 <h2 translate>device-profile.add</h2>
21 <span fxFlex></span> 21 <span fxFlex></span>
@@ -106,28 +106,25 @@ @@ -106,28 +106,25 @@
106 </mat-step> 106 </mat-step>
107 </mat-horizontal-stepper> 107 </mat-horizontal-stepper>
108 </div> 108 </div>
109 - <div mat-dialog-actions fxLayout="column" fxLayoutAlign="start wrap" fxLayoutGap="8px" style="height: 100px;">  
110 - <div fxFlex fxLayout="row" fxLayoutAlign="end">  
111 - <button mat-raised-button  
112 - *ngIf="showNext"  
113 - [disabled]="(isLoading$ | async)"  
114 - (click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>  
115 - </div>  
116 - <div fxFlex fxLayout="row">  
117 - <button mat-button  
118 - color="primary"  
119 - [disabled]="(isLoading$ | async)"  
120 - (click)="cancel()">{{ 'action.cancel' | translate }}</button>  
121 - <span fxFlex></span>  
122 - <div fxLayout="row wrap" fxLayoutGap="8px">  
123 - <button mat-raised-button *ngIf="selectedIndex > 0"  
124 - [disabled]="(isLoading$ | async)"  
125 - (click)="previousStep()">{{ 'action.back' | translate }}</button>  
126 - <button mat-raised-button  
127 - [disabled]="(isLoading$ | async)"  
128 - color="primary"  
129 - (click)="add()">{{ 'action.add' | translate }}</button>  
130 - </div>  
131 - </div> 109 + <div mat-dialog-actions fxLayout="row">
  110 + <button mat-stroked-button *ngIf="selectedIndex > 0"
  111 + [disabled]="(isLoading$ | async)"
  112 + (click)="previousStep()">{{ 'action.back' | translate }}</button>
  113 + <span fxFlex></span>
  114 + <button mat-stroked-button
  115 + color="primary"
  116 + *ngIf="showNext"
  117 + [disabled]="(isLoading$ | async)"
  118 + (click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
  119 + </div>
  120 + <mat-divider></mat-divider>
  121 + <div mat-dialog-actions fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end">
  122 + <button mat-button
  123 + [disabled]="(isLoading$ | async)"
  124 + (click)="cancel()">{{ 'action.cancel' | translate }}</button>
  125 + <button mat-raised-button
  126 + [disabled]="(isLoading$ | async)"
  127 + color="primary"
  128 + (click)="add()">{{ 'action.add' | translate }}</button>
132 </div> 129 </div>
133 </div> 130 </div>
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 display: flex; 28 display: flex;
29 flex-direction: column; 29 flex-direction: column;
30 height: 100%; 30 height: 100%;
31 - padding: 24px 24px 8px !important; 31 + padding: 0 !important;
32 32
33 .mat-stepper-horizontal { 33 .mat-stepper-horizontal {
34 display: flex; 34 display: flex;
@@ -45,7 +45,7 @@ @@ -45,7 +45,7 @@
45 } 45 }
46 } 46 }
47 .mat-horizontal-content-container { 47 .mat-horizontal-content-container {
48 - height: 350px; 48 + height: 530px;
49 max-height: 100%; 49 max-height: 100%;
50 width: 100%;; 50 width: 100%;;
51 overflow-y: auto; 51 overflow-y: auto;
@@ -15,25 +15,22 @@ @@ -15,25 +15,22 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div fxLayout="column" fxFlex [formGroup]="alarmRuleConditionFormGroup">  
19 - <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">  
20 - <label class="tb-title" translate>device-profile.condition</label>  
21 - <span fxFlex></span>  
22 - <a mat-button color="primary"  
23 - type="button"  
24 - (click)="openFilterDialog($event)"  
25 - matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"  
26 - matTooltipPosition="above">  
27 - {{ (disabled ? 'action.view' : (conditionSet() ? 'action.edit' : 'action.add')) | translate }}  
28 - </a>  
29 - </div> 18 +<div fxLayout="row" fxLayoutAlign="start center" [formGroup]="alarmRuleConditionFormGroup" style="min-width: 0;">
30 <div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)"> 19 <div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)">
31 <tb-filter-text formControlName="condition" 20 <tb-filter-text formControlName="condition"
  21 + [nowrap]="true"
32 required 22 required
33 addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}"> 23 addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
34 </tb-filter-text> 24 </tb-filter-text>
35 - <span class="tb-alarm-rule-condition-spec" [ngClass]="{disabled: this.disabled}" [innerHTML]="specText"> 25 + <span *ngIf="specText" class="tb-alarm-rule-condition-spec" [ngClass]="{disabled: this.disabled}" [innerHTML]="specText">
36 </span> 26 </span>
37 </div> 27 </div>
38 - 28 + <button mat-icon-button
  29 + [color]="conditionSet() ? 'primary' : 'warn'"
  30 + type="button"
  31 + (click)="openFilterDialog($event)"
  32 + matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
  33 + matTooltipPosition="above">
  34 + <mat-icon>{{ disabled ? 'visibility' : (conditionSet() ? 'edit' : 'add') }}</mat-icon>
  35 + </button>
39 </div> 36 </div>
@@ -22,13 +22,10 @@ @@ -22,13 +22,10 @@
22 } 22 }
23 .tb-alarm-rule-condition { 23 .tb-alarm-rule-condition {
24 cursor: pointer; 24 cursor: pointer;
  25 + min-width: 0;
25 .tb-alarm-rule-condition-spec { 26 .tb-alarm-rule-condition-spec {
26 - margin-top: 1em;  
27 - line-height: 1.8em; 27 + opacity: 0.7;
28 padding: 4px; 28 padding: 4px;
29 - &.disabled {  
30 - opacity: 0.7;  
31 - }  
32 } 29 }
33 } 30 }
34 } 31 }
@@ -16,36 +16,23 @@ @@ -16,36 +16,23 @@
16 16
17 --> 17 -->
18 <div fxLayout="column" [formGroup]="alarmRuleFormGroup"> 18 <div fxLayout="column" [formGroup]="alarmRuleFormGroup">
19 - <tb-alarm-rule-condition fxFlex class="row"  
20 - formControlName="condition"> 19 + <tb-alarm-rule-condition formControlName="condition">
21 </tb-alarm-rule-condition> 20 </tb-alarm-rule-condition>
22 - <mat-divider class="row"></mat-divider>  
23 - <tb-alarm-schedule-info fxFlex class="row"  
24 - formControlName="schedule"> 21 + <tb-alarm-schedule-info formControlName="schedule">
25 </tb-alarm-schedule-info> 22 </tb-alarm-schedule-info>
26 - <mat-divider class="row"></mat-divider>  
27 - <div fxLayout="column" fxFlex class="tb-alarm-rule-details row">  
28 - <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">  
29 - <label class="tb-title" translate>device-profile.alarm-rule-details</label>  
30 - <span fxFlex></span>  
31 - <a mat-button color="primary"  
32 - *ngIf="!disabled"  
33 - type="button"  
34 - (click)="openEditDetailsDialog($event)"  
35 - matTooltip="{{ 'action.edit' | translate }}"  
36 - matTooltipPosition="above">  
37 - {{ 'action.edit' | translate }}  
38 - </a>  
39 - </div>  
40 - <div fxLayout="row" fxLayoutAlign="start start">  
41 - <div class="tb-alarm-rule-details-content" [ngClass]="{disabled: this.disabled, collapsed: !this.expandAlarmDetails}"  
42 - (click)="!disabled ? openEditDetailsDialog($event) : {}"  
43 - fxFlex [innerHTML]="alarmRuleFormGroup.get('alarmDetails').value"></div>  
44 - <a mat-button color="primary"  
45 - type="button"  
46 - (click)="expandAlarmDetails = !expandAlarmDetails">  
47 - {{ (expandAlarmDetails ? 'action.hide' : 'action.read-more') | translate }}  
48 - </a>  
49 - </div> 23 + <div *ngIf="!disabled || alarmRuleFormGroup.get('alarmDetails').value" fxLayout="row" fxLayoutAlign="start center">
  24 + <span class="tb-alarm-rule-details title" (click)="openEditDetailsDialog($event)">
  25 + {{ alarmRuleFormGroup.get('alarmDetails').value ? ('device-profile.alarm-rule-details' | translate) + ': ' : ('device-profile.add-alarm-rule-details' | translate) }}
  26 + </span>
  27 + <span *ngIf="alarmRuleFormGroup.get('alarmDetails').value" class="tb-alarm-rule-details"
  28 + (click)="openEditDetailsDialog($event)"
  29 + [innerHTML]="alarmRuleFormGroup.get('alarmDetails').value"></span>
  30 + <button mat-icon-button color="primary"
  31 + type="button"
  32 + (click)="openEditDetailsDialog($event)"
  33 + matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
  34 + matTooltipPosition="above">
  35 + <mat-icon>{{ disabled ? 'visibility' : (alarmRuleFormGroup.get('alarmDetails').value ? 'edit' : 'add') }}</mat-icon>
  36 + </button>
50 </div> 37 </div>
51 </div> 38 </div>
@@ -14,31 +14,19 @@ @@ -14,31 +14,19 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 :host { 16 :host {
  17 + min-width: 0;
17 .row { 18 .row {
18 margin-top: 1em; 19 margin-top: 1em;
19 } 20 }
20 .tb-alarm-rule-details { 21 .tb-alarm-rule-details {
21 - a.mat-button {  
22 - &:hover, &:focus {  
23 - border-bottom: none;  
24 - }  
25 - }  
26 - .tb-alarm-rule-details-content {  
27 - min-height: 33px;  
28 - overflow: hidden;  
29 - white-space: pre;  
30 - line-height: 1.8em;  
31 - padding: 4px;  
32 - cursor: pointer;  
33 - &.collapsed {  
34 - max-height: 33px;  
35 - white-space: nowrap;  
36 - text-overflow: ellipsis;  
37 - }  
38 - &.disabled {  
39 - opacity: 0.7;  
40 - cursor: auto;  
41 - } 22 + padding: 4px;
  23 + cursor: pointer;
  24 + overflow: hidden;
  25 + white-space: nowrap;
  26 + text-overflow: ellipsis;
  27 + &.title {
  28 + opacity: 0.7;
  29 + overflow: visible;
42 } 30 }
43 } 31 }
44 } 32 }
@@ -118,7 +118,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat @@ -118,7 +118,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
118 disableClose: true, 118 disableClose: true,
119 panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], 119 panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
120 data: { 120 data: {
121 - alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value 121 + alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value,
  122 + readonly: this.disabled
122 } 123 }
123 }).afterClosed().subscribe((alarmDetails) => { 124 }).afterClosed().subscribe((alarmDetails) => {
124 if (isDefinedAndNotNull(alarmDetails)) { 125 if (isDefinedAndNotNull(alarmDetails)) {
@@ -15,19 +15,16 @@ @@ -15,19 +15,16 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div fxLayout="column" fxFlex>  
19 - <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">  
20 - <label class="tb-title" translate>device-profile.schedule</label>  
21 - <span fxFlex></span>  
22 - <a mat-button color="primary"  
23 - type="button"  
24 - (click)="openScheduleDialog($event)"  
25 - matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"  
26 - matTooltipPosition="above">  
27 - {{ (disabled ? 'action.view' : 'action.edit' ) | translate }}  
28 - </a>  
29 - </div>  
30 - <sapn class="tb-alarm-rule-schedule" [ngClass]="{disabled: this.disabled}" (click)="openScheduleDialog($event)" 18 +<div fxLayout="row" fxLayoutAlign="start center" style="min-width: 0;">
  19 + <span class="tb-alarm-rule-schedule title" (click)="openScheduleDialog($event)">{{('device-profile.schedule' | translate) + ': '}}</span>
  20 + <span class="tb-alarm-rule-schedule" (click)="openScheduleDialog($event)"
31 [innerHTML]="scheduleText"> 21 [innerHTML]="scheduleText">
32 - </sapn> 22 + </span>
  23 + <button mat-icon-button color="primary"
  24 + type="button"
  25 + (click)="openScheduleDialog($event)"
  26 + matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
  27 + matTooltipPosition="above">
  28 + <mat-icon>{{ disabled ? 'visibility' : 'edit' }}</mat-icon>
  29 + </button>
33 </div> 30 </div>
@@ -21,14 +21,14 @@ @@ -21,14 +21,14 @@
21 } 21 }
22 } 22 }
23 .tb-alarm-rule-schedule { 23 .tb-alarm-rule-schedule {
24 - line-height: 1.8em;  
25 padding: 4px; 24 padding: 4px;
26 cursor: pointer; 25 cursor: pointer;
27 - &.disabled { 26 + overflow: hidden;
  27 + white-space: nowrap;
  28 + text-overflow: ellipsis;
  29 + &.title {
28 opacity: 0.7; 30 opacity: 0.7;
29 - }  
30 - .nowrap {  
31 - white-space: nowrap; 31 + overflow: visible;
32 } 32 }
33 } 33 }
34 } 34 }
@@ -101,7 +101,7 @@ export class AlarmScheduleInfoComponent implements ControlValueAccessor, OnInit @@ -101,7 +101,7 @@ export class AlarmScheduleInfoComponent implements ControlValueAccessor, OnInit
101 for (const item of schedule.items) { 101 for (const item of schedule.items) {
102 if (item.enabled) { 102 if (item.enabled) {
103 if (this.scheduleText.length) { 103 if (this.scheduleText.length) {
104 - this.scheduleText += '<br/>'; 104 + this.scheduleText += ', ';
105 } 105 }
106 this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]); 106 this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]);
107 this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn), 107 this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn),
@@ -19,7 +19,7 @@ @@ -19,7 +19,7 @@
19 <div *ngFor="let createAlarmRuleControl of createAlarmRulesFormArray().controls; let $index = index; 19 <div *ngFor="let createAlarmRuleControl of createAlarmRulesFormArray().controls; let $index = index;
20 last as isLast;" fxLayout="row" fxLayoutAlign="start center" 20 last as isLast;" fxLayout="row" fxLayoutAlign="start center"
21 fxLayoutGap="8px" style="padding-bottom: 8px;" [formGroup]="createAlarmRuleControl"> 21 fxLayoutGap="8px" style="padding-bottom: 8px;" [formGroup]="createAlarmRuleControl">
22 - <div class="create-alarm-rule" fxFlex fxLayout="column" fxLayoutGap="8px" fxLayoutAlign="start"> 22 + <div class="create-alarm-rule" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
23 <mat-form-field class="severity mat-block" floatLabel="always" hideRequiredMarker> 23 <mat-form-field class="severity mat-block" floatLabel="always" hideRequiredMarker>
24 <mat-label translate>alarm.severity</mat-label> 24 <mat-label translate>alarm.severity</mat-label>
25 <mat-select formControlName="severity" 25 <mat-select formControlName="severity"
@@ -34,7 +34,7 @@ @@ -34,7 +34,7 @@
34 {{ 'device-profile.alarm-severity-required' | translate }} 34 {{ 'device-profile.alarm-severity-required' | translate }}
35 </mat-error> 35 </mat-error>
36 </mat-form-field> 36 </mat-form-field>
37 - <mat-divider></mat-divider> 37 + <mat-divider vertical></mat-divider>
38 <tb-alarm-rule formControlName="alarmRule" required fxFlex> 38 <tb-alarm-rule formControlName="alarmRule" required fxFlex>
39 </tb-alarm-rule> 39 </tb-alarm-rule>
40 </div> 40 </div>
@@ -98,7 +98,7 @@ @@ -98,7 +98,7 @@
98 <mat-icon>remove_circle_outline</mat-icon> 98 <mat-icon>remove_circle_outline</mat-icon>
99 </button> 99 </button>
100 </div> 100 </div>
101 - <div *ngIf="!alarmFormGroup.get('clearRule').value"> 101 + <div *ngIf="disabled && !alarmFormGroup.get('clearRule').value">
102 <span translate fxLayoutAlign="center center" style="margin: 16px 0" 102 <span translate fxLayoutAlign="center center" style="margin: 16px 0"
103 class="tb-prompt">device-profile.no-clear-alarm-rule</span> 103 class="tb-prompt">device-profile.no-clear-alarm-rule</span>
104 </div> 104 </div>
@@ -38,16 +38,16 @@ @@ -38,16 +38,16 @@
38 </fieldset> 38 </fieldset>
39 </div> 39 </div>
40 <div mat-dialog-actions fxLayoutAlign="end center"> 40 <div mat-dialog-actions fxLayoutAlign="end center">
41 - <button mat-raised-button color="primary"  
42 - type="submit"  
43 - [disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty">  
44 - {{ 'action.save' | translate }}  
45 - </button>  
46 <button mat-button color="primary" 41 <button mat-button color="primary"
47 type="button" 42 type="button"
48 [disabled]="(isLoading$ | async)" 43 [disabled]="(isLoading$ | async)"
49 (click)="cancel()" cdkFocusInitial> 44 (click)="cancel()" cdkFocusInitial>
50 {{ 'action.cancel' | translate }} 45 {{ 'action.cancel' | translate }}
51 </button> 46 </button>
  47 + <button *ngIf="!data.readonly" mat-raised-button color="primary"
  48 + type="submit"
  49 + [disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty">
  50 + {{ 'action.save' | translate }}
  51 + </button>
52 </div> 52 </div>
53 </form> 53 </form>
@@ -27,6 +27,7 @@ import { TranslateService } from '@ngx-translate/core'; @@ -27,6 +27,7 @@ import { TranslateService } from '@ngx-translate/core';
27 27
28 export interface EditAlarmDetailsDialogData { 28 export interface EditAlarmDetailsDialogData {
29 alarmDetails: string; 29 alarmDetails: string;
  30 + readonly: boolean;
30 } 31 }
31 32
32 @Component({ 33 @Component({
@@ -57,6 +58,9 @@ export class EditAlarmDetailsDialogComponent extends DialogComponent<EditAlarmDe @@ -57,6 +58,9 @@ export class EditAlarmDetailsDialogComponent extends DialogComponent<EditAlarmDe
57 this.editDetailsFormGroup = this.fb.group({ 58 this.editDetailsFormGroup = this.fb.group({
58 alarmDetails: [this.alarmDetails] 59 alarmDetails: [this.alarmDetails]
59 }); 60 });
  61 + if (this.data.readonly) {
  62 + this.editDetailsFormGroup.disable();
  63 + }
60 } 64 }
61 65
62 ngOnInit(): void { 66 ngOnInit(): void {
@@ -151,28 +151,25 @@ @@ -151,28 +151,25 @@
151 </mat-step> 151 </mat-step>
152 </mat-horizontal-stepper> 152 </mat-horizontal-stepper>
153 </div> 153 </div>
154 - <div mat-dialog-actions fxLayout="column" fxLayoutAlign="start wrap" fxLayoutGap="8px" style="height: 100px;">  
155 - <div fxFlex fxLayout="row" fxLayoutAlign="end">  
156 - <button mat-raised-button  
157 - *ngIf="showNext"  
158 - [disabled]="(isLoading$ | async)"  
159 - (click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>  
160 - </div>  
161 - <div fxFlex fxLayout="row">  
162 - <button mat-button  
163 - color="primary"  
164 - [disabled]="(isLoading$ | async)"  
165 - (click)="cancel()">{{ 'action.cancel' | translate }}</button>  
166 - <span fxFlex></span>  
167 - <div fxLayout="row wrap" fxLayoutGap="8px">  
168 - <button mat-raised-button *ngIf="selectedIndex > 0"  
169 - [disabled]="(isLoading$ | async)"  
170 - (click)="previousStep()">{{ 'action.back' | translate }}</button>  
171 - <button mat-raised-button  
172 - [disabled]="(isLoading$ | async)"  
173 - color="primary"  
174 - (click)="add()">{{ 'action.add' | translate }}</button>  
175 - </div>  
176 - </div> 154 + <div mat-dialog-actions fxLayout="row">
  155 + <button mat-stroked-button *ngIf="selectedIndex > 0"
  156 + [disabled]="(isLoading$ | async)"
  157 + (click)="previousStep()">{{ 'action.back' | translate }}</button>
  158 + <span fxFlex></span>
  159 + <button mat-stroked-button
  160 + color="primary"
  161 + *ngIf="showNext"
  162 + [disabled]="(isLoading$ | async)"
  163 + (click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
  164 + </div>
  165 + <mat-divider></mat-divider>
  166 + <div mat-dialog-actions fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end">
  167 + <button mat-button
  168 + [disabled]="(isLoading$ | async)"
  169 + (click)="cancel()">{{ 'action.cancel' | translate }}</button>
  170 + <button mat-raised-button
  171 + [disabled]="(isLoading$ | async)"
  172 + color="primary"
  173 + (click)="add()">{{ 'action.add' | translate }}</button>
177 </div> 174 </div>
178 </div> 175 </div>
@@ -29,6 +29,7 @@ @@ -29,6 +29,7 @@
29 display: flex; 29 display: flex;
30 flex-direction: column; 30 flex-direction: column;
31 height: 100%; 31 height: 100%;
  32 + padding: 0 !important;
32 33
33 .mat-stepper-horizontal { 34 .mat-stepper-horizontal {
34 display: flex; 35 display: flex;
@@ -45,7 +46,7 @@ @@ -45,7 +46,7 @@
45 } 46 }
46 } 47 }
47 .mat-horizontal-content-container { 48 .mat-horizontal-content-container {
48 - height: 450px; 49 + height: 530px;
49 max-height: 100%; 50 max-height: 100%;
50 width: 100%;; 51 width: 100%;;
51 overflow-y: auto; 52 overflow-y: auto;
@@ -923,6 +923,7 @@ @@ -923,6 +923,7 @@
923 "condition-duration-time-unit-required": "Time unit is required.", 923 "condition-duration-time-unit-required": "Time unit is required.",
924 "advanced-settings": "Advanced settings", 924 "advanced-settings": "Advanced settings",
925 "alarm-rule-details": "Details", 925 "alarm-rule-details": "Details",
  926 + "add-alarm-rule-details": "Add details",
926 "propagate-alarm": "Propagate alarm", 927 "propagate-alarm": "Propagate alarm",
927 "alarm-rule-relation-types-list": "Relation types to propagate", 928 "alarm-rule-relation-types-list": "Relation types to propagate",
928 "alarm-rule-relation-types-list-hint": "If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.", 929 "alarm-rule-relation-types-list-hint": "If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.",
@@ -944,14 +945,14 @@ @@ -944,14 +945,14 @@
944 "condition-type": "Condition type", 945 "condition-type": "Condition type",
945 "condition-type-simple": "Simple", 946 "condition-type-simple": "Simple",
946 "condition-type-duration": "Duration", 947 "condition-type-duration": "Duration",
947 - "condition-during": "During <b>{{during}}</b>", 948 + "condition-during": "During {{during}}",
948 "condition-type-repeating": "Repeating", 949 "condition-type-repeating": "Repeating",
949 "condition-type-required": "Condition type is required.", 950 "condition-type-required": "Condition type is required.",
950 "condition-repeating-value": "Count of events", 951 "condition-repeating-value": "Count of events",
951 "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", 952 "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.",
952 "condition-repeating-value-pattern": "Count of events should be integers.", 953 "condition-repeating-value-pattern": "Count of events should be integers.",
953 "condition-repeating-value-required": "Count of events is required.", 954 "condition-repeating-value-required": "Count of events is required.",
954 - "condition-repeat-times": "Repeats <b>{ count, plural, 1 {1 time} other {# times} }</b>", 955 + "condition-repeat-times": "Repeats { count, plural, 1 {1 time} other {# times} }",
955 "schedule-type": "Scheduler type", 956 "schedule-type": "Scheduler type",
956 "schedule-type-required": "Scheduler type is required.", 957 "schedule-type-required": "Scheduler type is required.",
957 "schedule": "Schedule", 958 "schedule": "Schedule",
@@ -869,10 +869,7 @@ mat-label { @@ -869,10 +869,7 @@ mat-label {
869 } 869 }
870 .mat-dialog-actions { 870 .mat-dialog-actions {
871 margin-bottom: 0; 871 margin-bottom: 0;
872 - padding: 8px 8px 8px 16px;  
873 - button:last-of-type{  
874 - margin-right: 20px;  
875 - } 872 + padding: 8px;
876 } 873 }
877 } 874 }
878 } 875 }