Oauth2 - set provider name to created user additionalInfo. UI: Improve device profile alarm rules.
This commit is contained in:
parent
b0126a9d47
commit
52e6e76ac6
@ -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(""));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -21,6 +21,11 @@
|
|||||||
}
|
}
|
||||||
&.required {
|
&.required {
|
||||||
color: #f44336;
|
color: #f44336;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
&.nowrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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)"
|
||||||
|
(click)="previousStep()">{{ 'action.back' | translate }}</button>
|
||||||
|
<span fxFlex></span>
|
||||||
|
<button mat-stroked-button
|
||||||
|
color="primary"
|
||||||
*ngIf="showNext"
|
*ngIf="showNext"
|
||||||
[disabled]="(isLoading$ | async)"
|
[disabled]="(isLoading$ | async)"
|
||||||
(click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
|
(click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div fxFlex fxLayout="row">
|
<mat-divider></mat-divider>
|
||||||
|
<div mat-dialog-actions fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end">
|
||||||
<button mat-button
|
<button mat-button
|
||||||
color="primary"
|
|
||||||
[disabled]="(isLoading$ | async)"
|
[disabled]="(isLoading$ | async)"
|
||||||
(click)="cancel()">{{ 'action.cancel' | translate }}</button>
|
(click)="cancel()">{{ 'action.cancel' | translate }}</button>
|
||||||
<span fxFlex></span>
|
|
||||||
<div fxLayout="row wrap" fxLayoutGap="8px">
|
|
||||||
<button mat-raised-button *ngIf="selectedIndex > 0"
|
|
||||||
[disabled]="(isLoading$ | async)"
|
|
||||||
(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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;">
|
<div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)">
|
||||||
<label class="tb-title" translate>device-profile.condition</label>
|
<tb-filter-text formControlName="condition"
|
||||||
<span fxFlex></span>
|
[nowrap]="true"
|
||||||
<a mat-button color="primary"
|
required
|
||||||
|
addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
|
||||||
|
</tb-filter-text>
|
||||||
|
<span *ngIf="specText" class="tb-alarm-rule-condition-spec" [ngClass]="{disabled: this.disabled}" [innerHTML]="specText">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button mat-icon-button
|
||||||
|
[color]="conditionSet() ? 'primary' : 'warn'"
|
||||||
type="button"
|
type="button"
|
||||||
(click)="openFilterDialog($event)"
|
(click)="openFilterDialog($event)"
|
||||||
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
|
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
|
||||||
matTooltipPosition="above">
|
matTooltipPosition="above">
|
||||||
{{ (disabled ? 'action.view' : (conditionSet() ? 'action.edit' : 'action.add')) | translate }}
|
<mat-icon>{{ disabled ? 'visibility' : (conditionSet() ? 'edit' : 'add') }}</mat-icon>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
|
||||||
<div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)">
|
|
||||||
<tb-filter-text formControlName="condition"
|
|
||||||
required
|
|
||||||
addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
|
|
||||||
</tb-filter-text>
|
|
||||||
<span class="tb-alarm-rule-condition-spec" [ngClass]="{disabled: this.disabled}" [innerHTML]="specText">
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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;
|
|
||||||
line-height: 1.8em;
|
|
||||||
padding: 4px;
|
|
||||||
&.disabled {
|
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
padding: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
<button mat-icon-button color="primary"
|
||||||
type="button"
|
type="button"
|
||||||
(click)="openEditDetailsDialog($event)"
|
(click)="openEditDetailsDialog($event)"
|
||||||
matTooltip="{{ 'action.edit' | translate }}"
|
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
|
||||||
matTooltipPosition="above">
|
matTooltipPosition="above">
|
||||||
{{ 'action.edit' | translate }}
|
<mat-icon>{{ disabled ? 'visibility' : (alarmRuleFormGroup.get('alarmDetails').value ? 'edit' : 'add') }}</mat-icon>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
|
||||||
<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>
|
||||||
|
|||||||
@ -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 {
|
|
||||||
&:hover, &:focus {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tb-alarm-rule-details-content {
|
|
||||||
min-height: 33px;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: pre;
|
|
||||||
line-height: 1.8em;
|
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&.collapsed {
|
overflow: hidden;
|
||||||
max-height: 33px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
&.title {
|
||||||
&.disabled {
|
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
cursor: auto;
|
overflow: visible;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)) {
|
||||||
|
|||||||
@ -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>
|
[innerHTML]="scheduleText">
|
||||||
<a mat-button color="primary"
|
</span>
|
||||||
|
<button mat-icon-button color="primary"
|
||||||
type="button"
|
type="button"
|
||||||
(click)="openScheduleDialog($event)"
|
(click)="openScheduleDialog($event)"
|
||||||
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
|
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
|
||||||
matTooltipPosition="above">
|
matTooltipPosition="above">
|
||||||
{{ (disabled ? 'action.view' : 'action.edit' ) | translate }}
|
<mat-icon>{{ disabled ? 'visibility' : 'edit' }}</mat-icon>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
|
||||||
<sapn class="tb-alarm-rule-schedule" [ngClass]="{disabled: this.disabled}" (click)="openScheduleDialog($event)"
|
|
||||||
[innerHTML]="scheduleText">
|
|
||||||
</sapn>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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;
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
.nowrap {
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
&.title {
|
||||||
|
opacity: 0.7;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)"
|
||||||
|
(click)="previousStep()">{{ 'action.back' | translate }}</button>
|
||||||
|
<span fxFlex></span>
|
||||||
|
<button mat-stroked-button
|
||||||
|
color="primary"
|
||||||
*ngIf="showNext"
|
*ngIf="showNext"
|
||||||
[disabled]="(isLoading$ | async)"
|
[disabled]="(isLoading$ | async)"
|
||||||
(click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
|
(click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div fxFlex fxLayout="row">
|
<mat-divider></mat-divider>
|
||||||
|
<div mat-dialog-actions fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end">
|
||||||
<button mat-button
|
<button mat-button
|
||||||
color="primary"
|
|
||||||
[disabled]="(isLoading$ | async)"
|
[disabled]="(isLoading$ | async)"
|
||||||
(click)="cancel()">{{ 'action.cancel' | translate }}</button>
|
(click)="cancel()">{{ 'action.cancel' | translate }}</button>
|
||||||
<span fxFlex></span>
|
|
||||||
<div fxLayout="row wrap" fxLayoutGap="8px">
|
|
||||||
<button mat-raised-button *ngIf="selectedIndex > 0"
|
|
||||||
[disabled]="(isLoading$ | async)"
|
|
||||||
(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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user