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 31 import org.thingsboard.server.common.data.id.DashboardId;
32 32 import org.thingsboard.server.common.data.id.IdBased;
33 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 36 import org.thingsboard.server.common.data.page.PageData;
35 37 import org.thingsboard.server.common.data.page.PageLink;
36 38 import org.thingsboard.server.common.data.security.Authority;
... ... @@ -76,12 +78,15 @@ public abstract class AbstractOAuth2ClientMapper {
76 78
77 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 85 UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail());
81 86
82 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 90 throw new UsernameNotFoundException("User not found: " + oauth2User.getEmail());
86 91 }
87 92
... ... @@ -106,21 +111,28 @@ public abstract class AbstractOAuth2ClientMapper {
106 111 user.setFirstName(oauth2User.getFirstName());
107 112 user.setLastName(oauth2User.getLastName());
108 113
  114 + ObjectNode additionalInfo = objectMapper.createObjectNode();
  115 +
109 116 if (!StringUtils.isEmpty(oauth2User.getDefaultDashboardName())) {
110 117 Optional<DashboardId> dashboardIdOpt =
111 118 user.getAuthority() == Authority.TENANT_ADMIN ?
112 119 getDashboardId(tenantId, oauth2User.getDefaultDashboardName())
113 120 : getDashboardId(tenantId, customerId, oauth2User.getDefaultDashboardName());
114 121 if (dashboardIdOpt.isPresent()) {
115   - ObjectNode additionalInfo = objectMapper.createObjectNode();
116 122 additionalInfo.put("defaultDashboardFullscreen", oauth2User.isAlwaysFullScreen());
117 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 134 user = userService.saveUser(user);
123   - if (activateUser) {
  135 + if (config.isActivateUser()) {
124 136 UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId());
125 137 userService.activateUserCredentials(user.getTenantId(), userCredentials.getActivateToken(), passwordEncoder.encode(""));
126 138 }
... ...
... ... @@ -18,6 +18,7 @@ package org.thingsboard.server.service.security.auth.oauth2;
18 18 import lombok.extern.slf4j.Slf4j;
19 19 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
20 20 import org.springframework.stereotype.Service;
  21 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
21 22 import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
22 23 import org.thingsboard.server.dao.oauth2.OAuth2User;
23 24 import org.thingsboard.server.service.security.model.SecurityUser;
... ... @@ -29,11 +30,12 @@ import java.util.Map;
29 30 public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
30 31
31 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 35 Map<String, Object> attributes = token.getPrincipal().getAttributes();
34 36 String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
35 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 23 import org.springframework.stereotype.Service;
24 24 import org.springframework.util.StringUtils;
25 25 import org.springframework.web.client.RestTemplate;
  26 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
26 27 import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig;
27 28 import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
28 29 import org.thingsboard.server.dao.oauth2.OAuth2User;
... ... @@ -38,9 +39,10 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
38 39 private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
39 40
40 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 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 48 private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2CustomMapperConfig custom) {
... ...
... ... @@ -23,6 +23,7 @@ import org.springframework.boot.web.client.RestTemplateBuilder;
23 23 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
24 24 import org.springframework.stereotype.Service;
25 25 import org.springframework.web.client.RestTemplate;
  26 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
26 27 import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
27 28 import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
28 29 import org.thingsboard.server.dao.oauth2.OAuth2User;
... ... @@ -45,12 +46,13 @@ public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
45 46 private OAuth2Configuration oAuth2Configuration;
46 47
47 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 51 Map<String, String> githubMapperConfig = oAuth2Configuration.getGithubMapper();
50 52 String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken);
51 53 Map<String, Object> attributes = token.getPrincipal().getAttributes();
52 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 58 private synchronized String getEmail(String emailUrl, String oauth2Token) {
... ...
... ... @@ -16,9 +16,9 @@
16 16 package org.thingsboard.server.service.security.auth.oauth2;
17 17
18 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 20 import org.thingsboard.server.service.security.model.SecurityUser;
21 21
22 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 74 token.getPrincipal().getName());
75 75 OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(clientRegistration.getMapperConfig().getType());
76 76 SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(),
77   - clientRegistration.getMapperConfig());
  77 + clientRegistration);
78 78
79 79 JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
80 80 JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
... ... @@ -85,4 +85,4 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
85 85 URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8.toString()));
86 86 }
87 87 }
88   -}
\ No newline at end of file
  88 +}
... ...
... ... @@ -33,11 +33,6 @@
33 33 <tb-anchor #entityDetailsForm></tb-anchor>
34 34 </div>
35 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 36 <button mat-button color="primary"
42 37 type="button"
43 38 cdkFocusInitial
... ... @@ -45,5 +40,10 @@
45 40 (click)="cancel()">
46 41 {{ 'action.cancel' | translate }}
47 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 48 </div>
49 49 </form>
... ...
... ... @@ -15,5 +15,5 @@
15 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 19 [innerHTML]="filterText"></div>
... ...
... ... @@ -21,6 +21,11 @@
21 21 }
22 22 &.required {
23 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 54 @Input()
55 55 addFilterPrompt = this.translate.instant('filter.add-filter-prompt');
56 56
  57 + @Input()
  58 + nowrap = false;
  59 +
57 60 requiredClass = false;
58 61
59 62 private filterText: string;
... ...
... ... @@ -15,7 +15,7 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<div style="min-width: 1000px;">
  18 +<div>
19 19 <mat-toolbar color="primary">
20 20 <h2 translate>device-profile.add</h2>
21 21 <span fxFlex></span>
... ... @@ -106,28 +106,25 @@
106 106 </mat-step>
107 107 </mat-horizontal-stepper>
108 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 129 </div>
133 130 </div>
... ...
... ... @@ -28,7 +28,7 @@
28 28 display: flex;
29 29 flex-direction: column;
30 30 height: 100%;
31   - padding: 24px 24px 8px !important;
  31 + padding: 0 !important;
32 32
33 33 .mat-stepper-horizontal {
34 34 display: flex;
... ... @@ -45,7 +45,7 @@
45 45 }
46 46 }
47 47 .mat-horizontal-content-container {
48   - height: 350px;
  48 + height: 530px;
49 49 max-height: 100%;
50 50 width: 100%;;
51 51 overflow-y: auto;
... ...
... ... @@ -15,25 +15,22 @@
15 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 19 <div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)">
31 20 <tb-filter-text formControlName="condition"
  21 + [nowrap]="true"
32 22 required
33 23 addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
34 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 26 </span>
37 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 36 </div>
... ...
... ... @@ -22,13 +22,10 @@
22 22 }
23 23 .tb-alarm-rule-condition {
24 24 cursor: pointer;
  25 + min-width: 0;
25 26 .tb-alarm-rule-condition-spec {
26   - margin-top: 1em;
27   - line-height: 1.8em;
  27 + opacity: 0.7;
28 28 padding: 4px;
29   - &.disabled {
30   - opacity: 0.7;
31   - }
32 29 }
33 30 }
34 31 }
... ...
... ... @@ -16,36 +16,23 @@
16 16
17 17 -->
18 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 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 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 37 </div>
51 38 </div>
... ...
... ... @@ -14,31 +14,19 @@
14 14 * limitations under the License.
15 15 */
16 16 :host {
  17 + min-width: 0;
17 18 .row {
18 19 margin-top: 1em;
19 20 }
20 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 118 disableClose: true,
119 119 panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
120 120 data: {
121   - alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value
  121 + alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value,
  122 + readonly: this.disabled
122 123 }
123 124 }).afterClosed().subscribe((alarmDetails) => {
124 125 if (isDefinedAndNotNull(alarmDetails)) {
... ...
... ... @@ -15,19 +15,16 @@
15 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 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 30 </div>
... ...
... ... @@ -21,14 +21,14 @@
21 21 }
22 22 }
23 23 .tb-alarm-rule-schedule {
24   - line-height: 1.8em;
25 24 padding: 4px;
26 25 cursor: pointer;
27   - &.disabled {
  26 + overflow: hidden;
  27 + white-space: nowrap;
  28 + text-overflow: ellipsis;
  29 + &.title {
28 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 101 for (const item of schedule.items) {
102 102 if (item.enabled) {
103 103 if (this.scheduleText.length) {
104   - this.scheduleText += '<br/>';
  104 + this.scheduleText += ', ';
105 105 }
106 106 this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]);
107 107 this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn),
... ...
... ... @@ -19,7 +19,7 @@
19 19 <div *ngFor="let createAlarmRuleControl of createAlarmRulesFormArray().controls; let $index = index;
20 20 last as isLast;" fxLayout="row" fxLayoutAlign="start center"
21 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 23 <mat-form-field class="severity mat-block" floatLabel="always" hideRequiredMarker>
24 24 <mat-label translate>alarm.severity</mat-label>
25 25 <mat-select formControlName="severity"
... ... @@ -34,7 +34,7 @@
34 34 {{ 'device-profile.alarm-severity-required' | translate }}
35 35 </mat-error>
36 36 </mat-form-field>
37   - <mat-divider></mat-divider>
  37 + <mat-divider vertical></mat-divider>
38 38 <tb-alarm-rule formControlName="alarmRule" required fxFlex>
39 39 </tb-alarm-rule>
40 40 </div>
... ...
... ... @@ -98,7 +98,7 @@
98 98 <mat-icon>remove_circle_outline</mat-icon>
99 99 </button>
100 100 </div>
101   - <div *ngIf="!alarmFormGroup.get('clearRule').value">
  101 + <div *ngIf="disabled && !alarmFormGroup.get('clearRule').value">
102 102 <span translate fxLayoutAlign="center center" style="margin: 16px 0"
103 103 class="tb-prompt">device-profile.no-clear-alarm-rule</span>
104 104 </div>
... ...
... ... @@ -38,16 +38,16 @@
38 38 </fieldset>
39 39 </div>
40 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 41 <button mat-button color="primary"
47 42 type="button"
48 43 [disabled]="(isLoading$ | async)"
49 44 (click)="cancel()" cdkFocusInitial>
50 45 {{ 'action.cancel' | translate }}
51 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 52 </div>
53 53 </form>
... ...
... ... @@ -27,6 +27,7 @@ import { TranslateService } from '@ngx-translate/core';
27 27
28 28 export interface EditAlarmDetailsDialogData {
29 29 alarmDetails: string;
  30 + readonly: boolean;
30 31 }
31 32
32 33 @Component({
... ... @@ -57,6 +58,9 @@ export class EditAlarmDetailsDialogComponent extends DialogComponent<EditAlarmDe
57 58 this.editDetailsFormGroup = this.fb.group({
58 59 alarmDetails: [this.alarmDetails]
59 60 });
  61 + if (this.data.readonly) {
  62 + this.editDetailsFormGroup.disable();
  63 + }
60 64 }
61 65
62 66 ngOnInit(): void {
... ...
... ... @@ -151,28 +151,25 @@
151 151 </mat-step>
152 152 </mat-horizontal-stepper>
153 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 174 </div>
178 175 </div>
... ...
... ... @@ -29,6 +29,7 @@
29 29 display: flex;
30 30 flex-direction: column;
31 31 height: 100%;
  32 + padding: 0 !important;
32 33
33 34 .mat-stepper-horizontal {
34 35 display: flex;
... ... @@ -45,7 +46,7 @@
45 46 }
46 47 }
47 48 .mat-horizontal-content-container {
48   - height: 450px;
  49 + height: 530px;
49 50 max-height: 100%;
50 51 width: 100%;;
51 52 overflow-y: auto;
... ...
... ... @@ -923,6 +923,7 @@
923 923 "condition-duration-time-unit-required": "Time unit is required.",
924 924 "advanced-settings": "Advanced settings",
925 925 "alarm-rule-details": "Details",
  926 + "add-alarm-rule-details": "Add details",
926 927 "propagate-alarm": "Propagate alarm",
927 928 "alarm-rule-relation-types-list": "Relation types to propagate",
928 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 945 "condition-type": "Condition type",
945 946 "condition-type-simple": "Simple",
946 947 "condition-type-duration": "Duration",
947   - "condition-during": "During <b>{{during}}</b>",
  948 + "condition-during": "During {{during}}",
948 949 "condition-type-repeating": "Repeating",
949 950 "condition-type-required": "Condition type is required.",
950 951 "condition-repeating-value": "Count of events",
951 952 "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.",
952 953 "condition-repeating-value-pattern": "Count of events should be integers.",
953 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 956 "schedule-type": "Scheduler type",
956 957 "schedule-type-required": "Scheduler type is required.",
957 958 "schedule": "Schedule",
... ...
... ... @@ -869,10 +869,7 @@ mat-label {
869 869 }
870 870 .mat-dialog-actions {
871 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 }
... ...