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

This commit is contained in:
Igor Kulikov 2020-10-13 19:48:46 +03:00
parent b0126a9d47
commit 52e6e76ac6
28 changed files with 165 additions and 175 deletions

View File

@ -31,6 +31,8 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.IdBased;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
@ -76,12 +78,15 @@ public abstract class AbstractOAuth2ClientMapper {
private final Lock userCreationLock = new ReentrantLock(); private final Lock userCreationLock = new ReentrantLock();
protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, boolean allowUserCreation, boolean activateUser) { protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, OAuth2ClientRegistrationInfo clientRegistration) {
OAuth2MapperConfig config = clientRegistration.getMapperConfig();
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail()); UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail());
User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail()); User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail());
if (user == null && !allowUserCreation) { if (user == null && !config.isAllowUserCreation()) {
throw new UsernameNotFoundException("User not found: " + oauth2User.getEmail()); throw new UsernameNotFoundException("User not found: " + oauth2User.getEmail());
} }
@ -106,21 +111,28 @@ public abstract class AbstractOAuth2ClientMapper {
user.setFirstName(oauth2User.getFirstName()); user.setFirstName(oauth2User.getFirstName());
user.setLastName(oauth2User.getLastName()); user.setLastName(oauth2User.getLastName());
ObjectNode additionalInfo = objectMapper.createObjectNode();
if (!StringUtils.isEmpty(oauth2User.getDefaultDashboardName())) { if (!StringUtils.isEmpty(oauth2User.getDefaultDashboardName())) {
Optional<DashboardId> dashboardIdOpt = Optional<DashboardId> dashboardIdOpt =
user.getAuthority() == Authority.TENANT_ADMIN ? user.getAuthority() == Authority.TENANT_ADMIN ?
getDashboardId(tenantId, oauth2User.getDefaultDashboardName()) getDashboardId(tenantId, oauth2User.getDefaultDashboardName())
: getDashboardId(tenantId, customerId, oauth2User.getDefaultDashboardName()); : getDashboardId(tenantId, customerId, oauth2User.getDefaultDashboardName());
if (dashboardIdOpt.isPresent()) { if (dashboardIdOpt.isPresent()) {
ObjectNode additionalInfo = objectMapper.createObjectNode();
additionalInfo.put("defaultDashboardFullscreen", oauth2User.isAlwaysFullScreen()); additionalInfo.put("defaultDashboardFullscreen", oauth2User.isAlwaysFullScreen());
additionalInfo.put("defaultDashboardId", dashboardIdOpt.get().getId().toString()); additionalInfo.put("defaultDashboardId", dashboardIdOpt.get().getId().toString());
user.setAdditionalInfo(additionalInfo);
} }
} }
if (clientRegistration.getAdditionalInfo() != null &&
clientRegistration.getAdditionalInfo().has("providerName")) {
additionalInfo.put("authProviderName", clientRegistration.getAdditionalInfo().get("providerName").asText());
}
user.setAdditionalInfo(additionalInfo);
user = userService.saveUser(user); user = userService.saveUser(user);
if (activateUser) { if (config.isActivateUser()) {
UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()); UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId());
userService.activateUserCredentials(user.getTenantId(), userCredentials.getActivateToken(), passwordEncoder.encode("")); userService.activateUserCredentials(user.getTenantId(), userCredentials.getActivateToken(), passwordEncoder.encode(""));
} }

View File

@ -18,6 +18,7 @@ package org.thingsboard.server.service.security.auth.oauth2;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@ -29,11 +30,12 @@ import java.util.Map;
public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
@Override @Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) { public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) {
OAuth2MapperConfig config = clientRegistration.getMapperConfig();
Map<String, Object> attributes = token.getPrincipal().getAttributes(); Map<String, Object> attributes = token.getPrincipal().getAttributes();
String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config);
return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser()); return getOrCreateSecurityUserFromOAuth2User(oauth2User, clientRegistration);
} }
} }

View File

@ -23,6 +23,7 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.dao.oauth2.OAuth2User;
@ -38,9 +39,10 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
@Override @Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) { public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) {
OAuth2MapperConfig config = clientRegistration.getMapperConfig();
OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom()); OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom());
return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser()); return getOrCreateSecurityUserFromOAuth2User(oauth2User, clientRegistration);
} }
private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2CustomMapperConfig custom) { private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2CustomMapperConfig custom) {

View File

@ -23,6 +23,7 @@ import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.dao.oauth2.OAuth2User;
@ -45,12 +46,13 @@ public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
private OAuth2Configuration oAuth2Configuration; private OAuth2Configuration oAuth2Configuration;
@Override @Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) { public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) {
OAuth2MapperConfig config = clientRegistration.getMapperConfig();
Map<String, String> githubMapperConfig = oAuth2Configuration.getGithubMapper(); Map<String, String> githubMapperConfig = oAuth2Configuration.getGithubMapper();
String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken); String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken);
Map<String, Object> attributes = token.getPrincipal().getAttributes(); Map<String, Object> attributes = token.getPrincipal().getAttributes();
OAuth2User oAuth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); OAuth2User oAuth2User = BasicMapperUtils.getOAuth2User(email, attributes, config);
return getOrCreateSecurityUserFromOAuth2User(oAuth2User, config.isAllowUserCreation(), config.isActivateUser()); return getOrCreateSecurityUserFromOAuth2User(oAuth2User, clientRegistration);
} }
private synchronized String getEmail(String emailUrl, String oauth2Token) { private synchronized String getEmail(String emailUrl, String oauth2Token) {

View File

@ -16,9 +16,9 @@
package org.thingsboard.server.service.security.auth.oauth2; package org.thingsboard.server.service.security.auth.oauth2;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
public interface OAuth2ClientMapper { public interface OAuth2ClientMapper {
SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config); SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration);
} }

View File

@ -74,7 +74,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
token.getPrincipal().getName()); token.getPrincipal().getName());
OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(clientRegistration.getMapperConfig().getType()); OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(clientRegistration.getMapperConfig().getType());
SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(),
clientRegistration.getMapperConfig()); clientRegistration);
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);

View File

@ -33,11 +33,6 @@
<tb-anchor #entityDetailsForm></tb-anchor> <tb-anchor #entityDetailsForm></tb-anchor>
</div> </div>
<div mat-dialog-actions fxLayoutAlign="end center"> <div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || detailsForm?.invalid || !detailsForm?.dirty">
{{ 'action.add' | translate }}
</button>
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"
cdkFocusInitial cdkFocusInitial
@ -45,5 +40,10 @@
(click)="cancel()"> (click)="cancel()">
{{ 'action.cancel' | translate }} {{ 'action.cancel' | translate }}
</button> </button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || detailsForm?.invalid || !detailsForm?.dirty">
{{ 'action.add' | translate }}
</button>
</div> </div>
</form> </form>

View File

@ -15,5 +15,5 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-filter-text" [ngClass]="{disabled: disabled, required: requiredClass}" <div class="tb-filter-text" [ngClass]="{disabled: disabled, required: requiredClass, nowrap: nowrap}"
[innerHTML]="filterText"></div> [innerHTML]="filterText"></div>

View File

@ -21,6 +21,11 @@
} }
&.required { &.required {
color: #f44336; color: #f44336;
padding: 0 4px;
}
&.nowrap {
white-space: nowrap;
overflow: hidden;
} }
} }
} }

View File

@ -54,6 +54,9 @@ export class FilterTextComponent implements ControlValueAccessor, OnInit {
@Input() @Input()
addFilterPrompt = this.translate.instant('filter.add-filter-prompt'); addFilterPrompt = this.translate.instant('filter.add-filter-prompt');
@Input()
nowrap = false;
requiredClass = false; requiredClass = false;
private filterText: string; private filterText: string;

View File

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div style="min-width: 1000px;"> <div>
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2 translate>device-profile.add</h2> <h2 translate>device-profile.add</h2>
<span fxFlex></span> <span fxFlex></span>
@ -106,28 +106,25 @@
</mat-step> </mat-step>
</mat-horizontal-stepper> </mat-horizontal-stepper>
</div> </div>
<div mat-dialog-actions fxLayout="column" fxLayoutAlign="start wrap" fxLayoutGap="8px" style="height: 100px;"> <div mat-dialog-actions fxLayout="row">
<div fxFlex fxLayout="row" fxLayoutAlign="end"> <button mat-stroked-button *ngIf="selectedIndex > 0"
<button mat-raised-button [disabled]="(isLoading$ | async)"
*ngIf="showNext" (click)="previousStep()">{{ 'action.back' | translate }}</button>
[disabled]="(isLoading$ | async)" <span fxFlex></span>
(click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button> <button mat-stroked-button
</div> color="primary"
<div fxFlex fxLayout="row"> *ngIf="showNext"
<button mat-button [disabled]="(isLoading$ | async)"
color="primary" (click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
[disabled]="(isLoading$ | async)" </div>
(click)="cancel()">{{ 'action.cancel' | translate }}</button> <mat-divider></mat-divider>
<span fxFlex></span> <div mat-dialog-actions fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end">
<div fxLayout="row wrap" fxLayoutGap="8px"> <button mat-button
<button mat-raised-button *ngIf="selectedIndex > 0" [disabled]="(isLoading$ | async)"
[disabled]="(isLoading$ | async)" (click)="cancel()">{{ 'action.cancel' | translate }}</button>
(click)="previousStep()">{{ 'action.back' | translate }}</button> <button mat-raised-button
<button mat-raised-button [disabled]="(isLoading$ | async)"
[disabled]="(isLoading$ | async)" color="primary"
color="primary" (click)="add()">{{ 'action.add' | translate }}</button>
(click)="add()">{{ 'action.add' | translate }}</button>
</div>
</div>
</div> </div>
</div> </div>

View File

@ -28,7 +28,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
padding: 24px 24px 8px !important; padding: 0 !important;
.mat-stepper-horizontal { .mat-stepper-horizontal {
display: flex; display: flex;
@ -45,7 +45,7 @@
} }
} }
.mat-horizontal-content-container { .mat-horizontal-content-container {
height: 350px; height: 530px;
max-height: 100%; max-height: 100%;
width: 100%;; width: 100%;;
overflow-y: auto; overflow-y: auto;

View File

@ -15,25 +15,22 @@
limitations under the License. limitations under the License.
--> -->
<div fxLayout="column" fxFlex [formGroup]="alarmRuleConditionFormGroup"> <div fxLayout="row" fxLayoutAlign="start center" [formGroup]="alarmRuleConditionFormGroup" style="min-width: 0;">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<label class="tb-title" translate>device-profile.condition</label>
<span fxFlex></span>
<a mat-button color="primary"
type="button"
(click)="openFilterDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
{{ (disabled ? 'action.view' : (conditionSet() ? 'action.edit' : 'action.add')) | translate }}
</a>
</div>
<div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)"> <div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)">
<tb-filter-text formControlName="condition" <tb-filter-text formControlName="condition"
[nowrap]="true"
required required
addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}"> addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
</tb-filter-text> </tb-filter-text>
<span class="tb-alarm-rule-condition-spec" [ngClass]="{disabled: this.disabled}" [innerHTML]="specText"> <span *ngIf="specText" class="tb-alarm-rule-condition-spec" [ngClass]="{disabled: this.disabled}" [innerHTML]="specText">
</span> </span>
</div> </div>
<button mat-icon-button
[color]="conditionSet() ? 'primary' : 'warn'"
type="button"
(click)="openFilterDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
<mat-icon>{{ disabled ? 'visibility' : (conditionSet() ? 'edit' : 'add') }}</mat-icon>
</button>
</div> </div>

View File

@ -22,13 +22,10 @@
} }
.tb-alarm-rule-condition { .tb-alarm-rule-condition {
cursor: pointer; cursor: pointer;
min-width: 0;
.tb-alarm-rule-condition-spec { .tb-alarm-rule-condition-spec {
margin-top: 1em; opacity: 0.7;
line-height: 1.8em;
padding: 4px; padding: 4px;
&.disabled {
opacity: 0.7;
}
} }
} }
} }

View File

@ -16,36 +16,23 @@
--> -->
<div fxLayout="column" [formGroup]="alarmRuleFormGroup"> <div fxLayout="column" [formGroup]="alarmRuleFormGroup">
<tb-alarm-rule-condition fxFlex class="row" <tb-alarm-rule-condition formControlName="condition">
formControlName="condition">
</tb-alarm-rule-condition> </tb-alarm-rule-condition>
<mat-divider class="row"></mat-divider> <tb-alarm-schedule-info formControlName="schedule">
<tb-alarm-schedule-info fxFlex class="row"
formControlName="schedule">
</tb-alarm-schedule-info> </tb-alarm-schedule-info>
<mat-divider class="row"></mat-divider> <div *ngIf="!disabled || alarmRuleFormGroup.get('alarmDetails').value" fxLayout="row" fxLayoutAlign="start center">
<div fxLayout="column" fxFlex class="tb-alarm-rule-details row"> <span class="tb-alarm-rule-details title" (click)="openEditDetailsDialog($event)">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;"> {{ alarmRuleFormGroup.get('alarmDetails').value ? ('device-profile.alarm-rule-details' | translate) + ': ' : ('device-profile.add-alarm-rule-details' | translate) }}
<label class="tb-title" translate>device-profile.alarm-rule-details</label> </span>
<span fxFlex></span> <span *ngIf="alarmRuleFormGroup.get('alarmDetails').value" class="tb-alarm-rule-details"
<a mat-button color="primary" (click)="openEditDetailsDialog($event)"
*ngIf="!disabled" [innerHTML]="alarmRuleFormGroup.get('alarmDetails').value"></span>
type="button" <button mat-icon-button color="primary"
(click)="openEditDetailsDialog($event)" type="button"
matTooltip="{{ 'action.edit' | translate }}" (click)="openEditDetailsDialog($event)"
matTooltipPosition="above"> matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
{{ 'action.edit' | translate }} matTooltipPosition="above">
</a> <mat-icon>{{ disabled ? 'visibility' : (alarmRuleFormGroup.get('alarmDetails').value ? 'edit' : 'add') }}</mat-icon>
</div> </button>
<div fxLayout="row" fxLayoutAlign="start start">
<div class="tb-alarm-rule-details-content" [ngClass]="{disabled: this.disabled, collapsed: !this.expandAlarmDetails}"
(click)="!disabled ? openEditDetailsDialog($event) : {}"
fxFlex [innerHTML]="alarmRuleFormGroup.get('alarmDetails').value"></div>
<a mat-button color="primary"
type="button"
(click)="expandAlarmDetails = !expandAlarmDetails">
{{ (expandAlarmDetails ? 'action.hide' : 'action.read-more') | translate }}
</a>
</div>
</div> </div>
</div> </div>

View File

@ -14,31 +14,19 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
min-width: 0;
.row { .row {
margin-top: 1em; margin-top: 1em;
} }
.tb-alarm-rule-details { .tb-alarm-rule-details {
a.mat-button { padding: 4px;
&:hover, &:focus { cursor: pointer;
border-bottom: none; overflow: hidden;
} white-space: nowrap;
} text-overflow: ellipsis;
.tb-alarm-rule-details-content { &.title {
min-height: 33px; opacity: 0.7;
overflow: hidden; overflow: visible;
white-space: pre;
line-height: 1.8em;
padding: 4px;
cursor: pointer;
&.collapsed {
max-height: 33px;
white-space: nowrap;
text-overflow: ellipsis;
}
&.disabled {
opacity: 0.7;
cursor: auto;
}
} }
} }
} }

View File

@ -118,7 +118,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value,
readonly: this.disabled
} }
}).afterClosed().subscribe((alarmDetails) => { }).afterClosed().subscribe((alarmDetails) => {
if (isDefinedAndNotNull(alarmDetails)) { if (isDefinedAndNotNull(alarmDetails)) {

View File

@ -15,19 +15,16 @@
limitations under the License. limitations under the License.
--> -->
<div fxLayout="column" fxFlex> <div fxLayout="row" fxLayoutAlign="start center" style="min-width: 0;">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;"> <span class="tb-alarm-rule-schedule title" (click)="openScheduleDialog($event)">{{('device-profile.schedule' | translate) + ': '}}</span>
<label class="tb-title" translate>device-profile.schedule</label> <span class="tb-alarm-rule-schedule" (click)="openScheduleDialog($event)"
<span fxFlex></span>
<a mat-button color="primary"
type="button"
(click)="openScheduleDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
{{ (disabled ? 'action.view' : 'action.edit' ) | translate }}
</a>
</div>
<sapn class="tb-alarm-rule-schedule" [ngClass]="{disabled: this.disabled}" (click)="openScheduleDialog($event)"
[innerHTML]="scheduleText"> [innerHTML]="scheduleText">
</sapn> </span>
<button mat-icon-button color="primary"
type="button"
(click)="openScheduleDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
<mat-icon>{{ disabled ? 'visibility' : 'edit' }}</mat-icon>
</button>
</div> </div>

View File

@ -21,14 +21,14 @@
} }
} }
.tb-alarm-rule-schedule { .tb-alarm-rule-schedule {
line-height: 1.8em;
padding: 4px; padding: 4px;
cursor: pointer; cursor: pointer;
&.disabled { overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&.title {
opacity: 0.7; opacity: 0.7;
} overflow: visible;
.nowrap {
white-space: nowrap;
} }
} }
} }

View File

@ -101,7 +101,7 @@ export class AlarmScheduleInfoComponent implements ControlValueAccessor, OnInit
for (const item of schedule.items) { for (const item of schedule.items) {
if (item.enabled) { if (item.enabled) {
if (this.scheduleText.length) { if (this.scheduleText.length) {
this.scheduleText += '<br/>'; this.scheduleText += ', ';
} }
this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]); this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]);
this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn), this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn),

View File

@ -19,7 +19,7 @@
<div *ngFor="let createAlarmRuleControl of createAlarmRulesFormArray().controls; let $index = index; <div *ngFor="let createAlarmRuleControl of createAlarmRulesFormArray().controls; let $index = index;
last as isLast;" fxLayout="row" fxLayoutAlign="start center" last as isLast;" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px" style="padding-bottom: 8px;" [formGroup]="createAlarmRuleControl"> fxLayoutGap="8px" style="padding-bottom: 8px;" [formGroup]="createAlarmRuleControl">
<div class="create-alarm-rule" fxFlex fxLayout="column" fxLayoutGap="8px" fxLayoutAlign="start"> <div class="create-alarm-rule" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
<mat-form-field class="severity mat-block" floatLabel="always" hideRequiredMarker> <mat-form-field class="severity mat-block" floatLabel="always" hideRequiredMarker>
<mat-label translate>alarm.severity</mat-label> <mat-label translate>alarm.severity</mat-label>
<mat-select formControlName="severity" <mat-select formControlName="severity"
@ -34,7 +34,7 @@
{{ 'device-profile.alarm-severity-required' | translate }} {{ 'device-profile.alarm-severity-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-divider></mat-divider> <mat-divider vertical></mat-divider>
<tb-alarm-rule formControlName="alarmRule" required fxFlex> <tb-alarm-rule formControlName="alarmRule" required fxFlex>
</tb-alarm-rule> </tb-alarm-rule>
</div> </div>

View File

@ -98,7 +98,7 @@
<mat-icon>remove_circle_outline</mat-icon> <mat-icon>remove_circle_outline</mat-icon>
</button> </button>
</div> </div>
<div *ngIf="!alarmFormGroup.get('clearRule').value"> <div *ngIf="disabled && !alarmFormGroup.get('clearRule').value">
<span translate fxLayoutAlign="center center" style="margin: 16px 0" <span translate fxLayoutAlign="center center" style="margin: 16px 0"
class="tb-prompt">device-profile.no-clear-alarm-rule</span> class="tb-prompt">device-profile.no-clear-alarm-rule</span>
</div> </div>

View File

@ -38,16 +38,16 @@
</fieldset> </fieldset>
</div> </div>
<div mat-dialog-actions fxLayoutAlign="end center"> <div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial> (click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }} {{ 'action.cancel' | translate }}
</button> </button>
<button *ngIf="!data.readonly" mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div> </div>
</form> </form>

View File

@ -27,6 +27,7 @@ import { TranslateService } from '@ngx-translate/core';
export interface EditAlarmDetailsDialogData { export interface EditAlarmDetailsDialogData {
alarmDetails: string; alarmDetails: string;
readonly: boolean;
} }
@Component({ @Component({
@ -57,6 +58,9 @@ export class EditAlarmDetailsDialogComponent extends DialogComponent<EditAlarmDe
this.editDetailsFormGroup = this.fb.group({ this.editDetailsFormGroup = this.fb.group({
alarmDetails: [this.alarmDetails] alarmDetails: [this.alarmDetails]
}); });
if (this.data.readonly) {
this.editDetailsFormGroup.disable();
}
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -151,28 +151,25 @@
</mat-step> </mat-step>
</mat-horizontal-stepper> </mat-horizontal-stepper>
</div> </div>
<div mat-dialog-actions fxLayout="column" fxLayoutAlign="start wrap" fxLayoutGap="8px" style="height: 100px;"> <div mat-dialog-actions fxLayout="row">
<div fxFlex fxLayout="row" fxLayoutAlign="end"> <button mat-stroked-button *ngIf="selectedIndex > 0"
<button mat-raised-button [disabled]="(isLoading$ | async)"
*ngIf="showNext" (click)="previousStep()">{{ 'action.back' | translate }}</button>
[disabled]="(isLoading$ | async)" <span fxFlex></span>
(click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button> <button mat-stroked-button
</div> color="primary"
<div fxFlex fxLayout="row"> *ngIf="showNext"
<button mat-button [disabled]="(isLoading$ | async)"
color="primary" (click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
[disabled]="(isLoading$ | async)" </div>
(click)="cancel()">{{ 'action.cancel' | translate }}</button> <mat-divider></mat-divider>
<span fxFlex></span> <div mat-dialog-actions fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end">
<div fxLayout="row wrap" fxLayoutGap="8px"> <button mat-button
<button mat-raised-button *ngIf="selectedIndex > 0" [disabled]="(isLoading$ | async)"
[disabled]="(isLoading$ | async)" (click)="cancel()">{{ 'action.cancel' | translate }}</button>
(click)="previousStep()">{{ 'action.back' | translate }}</button> <button mat-raised-button
<button mat-raised-button [disabled]="(isLoading$ | async)"
[disabled]="(isLoading$ | async)" color="primary"
color="primary" (click)="add()">{{ 'action.add' | translate }}</button>
(click)="add()">{{ 'action.add' | translate }}</button>
</div>
</div>
</div> </div>
</div> </div>

View File

@ -29,6 +29,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
padding: 0 !important;
.mat-stepper-horizontal { .mat-stepper-horizontal {
display: flex; display: flex;
@ -45,7 +46,7 @@
} }
} }
.mat-horizontal-content-container { .mat-horizontal-content-container {
height: 450px; height: 530px;
max-height: 100%; max-height: 100%;
width: 100%;; width: 100%;;
overflow-y: auto; overflow-y: auto;

View File

@ -923,6 +923,7 @@
"condition-duration-time-unit-required": "Time unit is required.", "condition-duration-time-unit-required": "Time unit is required.",
"advanced-settings": "Advanced settings", "advanced-settings": "Advanced settings",
"alarm-rule-details": "Details", "alarm-rule-details": "Details",
"add-alarm-rule-details": "Add details",
"propagate-alarm": "Propagate alarm", "propagate-alarm": "Propagate alarm",
"alarm-rule-relation-types-list": "Relation types to propagate", "alarm-rule-relation-types-list": "Relation types to propagate",
"alarm-rule-relation-types-list-hint": "If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.", "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 @@
"condition-type": "Condition type", "condition-type": "Condition type",
"condition-type-simple": "Simple", "condition-type-simple": "Simple",
"condition-type-duration": "Duration", "condition-type-duration": "Duration",
"condition-during": "During <b>{{during}}</b>", "condition-during": "During {{during}}",
"condition-type-repeating": "Repeating", "condition-type-repeating": "Repeating",
"condition-type-required": "Condition type is required.", "condition-type-required": "Condition type is required.",
"condition-repeating-value": "Count of events", "condition-repeating-value": "Count of events",
"condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.",
"condition-repeating-value-pattern": "Count of events should be integers.", "condition-repeating-value-pattern": "Count of events should be integers.",
"condition-repeating-value-required": "Count of events is required.", "condition-repeating-value-required": "Count of events is required.",
"condition-repeat-times": "Repeats <b>{ count, plural, 1 {1 time} other {# times} }</b>", "condition-repeat-times": "Repeats { count, plural, 1 {1 time} other {# times} }",
"schedule-type": "Scheduler type", "schedule-type": "Scheduler type",
"schedule-type-required": "Scheduler type is required.", "schedule-type-required": "Scheduler type is required.",
"schedule": "Schedule", "schedule": "Schedule",

View File

@ -869,10 +869,7 @@ mat-label {
} }
.mat-dialog-actions { .mat-dialog-actions {
margin-bottom: 0; margin-bottom: 0;
padding: 8px 8px 8px 16px; padding: 8px;
button:last-of-type{
margin-right: 20px;
}
} }
} }
} }