Merge branch 'master' into feature/pie-chart
This commit is contained in:
commit
8b5469b0aa
@ -67,6 +67,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH;
|
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH;
|
||||||
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
|
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
|
||||||
@ -147,9 +148,9 @@ public class LwM2MTestClient {
|
|||||||
initializer.setClassForObject(SECURITY, Security.class);
|
initializer.setClassForObject(SECURITY, Security.class);
|
||||||
initializer.setInstancesForObject(SECURITY, instances);
|
initializer.setInstancesForObject(SECURITY, instances);
|
||||||
// SERVER
|
// SERVER
|
||||||
Server lwm2mServer = new Server(shortServerId, 300);
|
Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60));
|
||||||
lwm2mServer.setId(serverId);
|
lwm2mServer.setId(serverId);
|
||||||
Server serverBs = new Server(shortServerIdBs0, 300);
|
Server serverBs = new Server(shortServerIdBs0, TimeUnit.MINUTES.toSeconds(60));
|
||||||
serverBs.setId(serverIdBs);
|
serverBs.setId(serverIdBs);
|
||||||
instances = new LwM2mInstanceEnabler[]{serverBs, lwm2mServer};
|
instances = new LwM2mInstanceEnabler[]{serverBs, lwm2mServer};
|
||||||
initializer.setClassForObject(SERVER, Server.class);
|
initializer.setClassForObject(SERVER, Server.class);
|
||||||
@ -163,7 +164,7 @@ public class LwM2MTestClient {
|
|||||||
// SECURITY
|
// SECURITY
|
||||||
initializer.setInstancesForObject(SECURITY, security);
|
initializer.setInstancesForObject(SECURITY, security);
|
||||||
// SERVER
|
// SERVER
|
||||||
Server lwm2mServer = new Server(shortServerId, 300);
|
Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60));
|
||||||
lwm2mServer.setId(serverId);
|
lwm2mServer.setId(serverId);
|
||||||
initializer.setInstancesForObject(SERVER, lwm2mServer );
|
initializer.setInstancesForObject(SERVER, lwm2mServer );
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,7 +90,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab
|
|||||||
private static final Logger LOG = LoggerFactory.getLogger(RedisRegistrationStore.class);
|
private static final Logger LOG = LoggerFactory.getLogger(RedisRegistrationStore.class);
|
||||||
|
|
||||||
// Redis key prefixes
|
// Redis key prefixes
|
||||||
private static final String REG_EP = "REG:EP:"; // (Endpoint => Registration)
|
public static final String REG_EP = "REG:EP:"; // (Endpoint => Registration)
|
||||||
private static final String REG_EP_REGID_IDX = "EP:REGID:"; // secondary index key (Registration ID => Endpoint)
|
private static final String REG_EP_REGID_IDX = "EP:REGID:"; // secondary index key (Registration ID => Endpoint)
|
||||||
private static final String REG_EP_ADDR_IDX = "EP:ADDR:"; // secondary index key (Socket Address => Endpoint)
|
private static final String REG_EP_ADDR_IDX = "EP:ADDR:"; // secondary index key (Socket Address => Endpoint)
|
||||||
private static final String REG_EP_IDENTITY = "EP:IDENTITY:"; // secondary index key (Identity => Endpoint)
|
private static final String REG_EP_IDENTITY = "EP:IDENTITY:"; // secondary index key (Identity => Endpoint)
|
||||||
|
|||||||
@ -15,17 +15,24 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.transport.lwm2m.server.store;
|
package org.thingsboard.server.transport.lwm2m.server.store;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.eclipse.leshan.core.SecurityMode;
|
import org.eclipse.leshan.core.SecurityMode;
|
||||||
import org.eclipse.leshan.core.peer.OscoreIdentity;
|
import org.eclipse.leshan.core.peer.OscoreIdentity;
|
||||||
import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException;
|
import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException;
|
||||||
import org.eclipse.leshan.server.security.SecurityInfo;
|
import org.eclipse.leshan.server.security.SecurityInfo;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnection;
|
||||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
import org.springframework.integration.redis.util.RedisLockRegistry;
|
import org.springframework.integration.redis.util.RedisLockRegistry;
|
||||||
|
import org.thingsboard.common.util.JacksonUtil;
|
||||||
import org.thingsboard.server.common.data.JavaSerDesUtil;
|
import org.thingsboard.server.common.data.JavaSerDesUtil;
|
||||||
import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MSecurityInfo;
|
import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MSecurityInfo;
|
||||||
|
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
|
|
||||||
|
import static org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mRedisRegistrationStore.REG_EP;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore {
|
public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore {
|
||||||
private static final String SEC_EP = "SEC#EP#";
|
private static final String SEC_EP = "SEC#EP#";
|
||||||
private static final String LOCK_EP = "LOCK#EP#";
|
private static final String LOCK_EP = "LOCK#EP#";
|
||||||
@ -49,11 +56,31 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore {
|
|||||||
if (data == null || data.length == 0) {
|
if (data == null || data.length == 0) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
if (SecurityMode.NO_SEC.equals(((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(data)).getSecurityMode())) {
|
TbLwM2MSecurityInfo tbLwM2MSecurityInfo = JavaSerDesUtil.decode(data);
|
||||||
|
if (tbLwM2MSecurityInfo != null) {
|
||||||
|
if (SecurityMode.NO_SEC.equals(tbLwM2MSecurityInfo.getSecurityMode())){
|
||||||
|
|
||||||
|
// for tests: redis connect NoSec (securityInfo == null)
|
||||||
|
log.info("lwm2m redis securityStore (decode -ok). Endpoint: [{}], secMode: [NoSec] key: [{}], data [{}]", endpoint, SEC_EP, data);
|
||||||
|
|
||||||
return SecurityInfo.newPreSharedKeyInfo(SecurityMode.NO_SEC.toString(), SecurityMode.NO_SEC.toString(),
|
return SecurityInfo.newPreSharedKeyInfo(SecurityMode.NO_SEC.toString(), SecurityMode.NO_SEC.toString(),
|
||||||
SecurityMode.NO_SEC.toString().getBytes());
|
SecurityMode.NO_SEC.toString().getBytes());
|
||||||
} else {
|
} else {
|
||||||
return ((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(data)).getSecurityInfo();
|
return tbLwM2MSecurityInfo.getSecurityInfo();
|
||||||
|
}
|
||||||
|
} else if (SecurityMode.NO_SEC.equals(getSecurityModeByRegistration (connection, endpoint))){
|
||||||
|
|
||||||
|
// for tests: redis connect NoSec (securityInfo == null)
|
||||||
|
log.info("lwm2m redis securityStore (decode - bad, registration - unsecure). Endpoint: [{}], secMode: [NoSec] key: [{}], data [{}]", endpoint, SEC_EP, data);
|
||||||
|
|
||||||
|
return SecurityInfo.newPreSharedKeyInfo(SecurityMode.NO_SEC.toString(), SecurityMode.NO_SEC.toString(),
|
||||||
|
SecurityMode.NO_SEC.toString().getBytes());
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// for tests: redis connect NoSec (securityInfo == null)
|
||||||
|
log.info("lwm2m redis securityStore (decode - bad, registration is not unsecure) - return null. Endpoint: [{}], key: [{}], data [{}]", endpoint, SEC_EP, data);
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@ -112,6 +139,11 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
byte[] previousData = connection.getSet((SEC_EP + tbSecurityInfo.getEndpoint()).getBytes(), tbSecurityInfoSerialized);
|
byte[] previousData = connection.getSet((SEC_EP + tbSecurityInfo.getEndpoint()).getBytes(), tbSecurityInfoSerialized);
|
||||||
|
|
||||||
|
// for tests: redis connect NoSec (securityInfo == null)
|
||||||
|
log.info("lwm2m redis connect. Endpoint: [{}], secMode: [{}] key: [{}], tbSecurityInfoSerialized [{}]",
|
||||||
|
tbSecurityInfo.getEndpoint(), tbSecurityInfo.getSecurityMode().name(), SEC_EP, tbSecurityInfoSerialized);
|
||||||
|
|
||||||
if (previousData != null && info != null) {
|
if (previousData != null && info != null) {
|
||||||
String previousIdentity = ((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(previousData)).getSecurityInfo().getPskIdentity();
|
String previousIdentity = ((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(previousData)).getSecurityInfo().getPskIdentity();
|
||||||
if (previousIdentity != null && !previousIdentity.equals(info.getPskIdentity())) {
|
if (previousIdentity != null && !previousIdentity.equals(info.getPskIdentity())) {
|
||||||
@ -168,4 +200,17 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore {
|
|||||||
private String toLockKey(String endpoint) {
|
private String toLockKey(String endpoint) {
|
||||||
return LOCK_EP + endpoint;
|
return LOCK_EP + endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SecurityMode getSecurityModeByRegistration (RedisConnection connection, String endpoint) {
|
||||||
|
try {
|
||||||
|
byte[] data = connection.get((REG_EP + endpoint).getBytes());
|
||||||
|
JsonNode registrationNode = JacksonUtil.fromString(new String(data != null ? data : new byte[0]), JsonNode.class);
|
||||||
|
String typeModeStr = registrationNode.get("transportdata").get("identity").get("type").asText();
|
||||||
|
return "unsecure".equals(typeModeStr) ? SecurityMode.NO_SEC : null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Redis: Failed get SecurityMode by Registration, endpoint: [{}]", endpoint, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,6 +61,7 @@ import java.util.HashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH;
|
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH;
|
||||||
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
|
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
|
||||||
@ -118,7 +119,7 @@ public class LwM2MTestClient {
|
|||||||
// SECURITY
|
// SECURITY
|
||||||
initializer.setInstancesForObject(SECURITY, security);
|
initializer.setInstancesForObject(SECURITY, security);
|
||||||
// SERVER
|
// SERVER
|
||||||
Server lwm2mServer = new Server(shortServerId, 300);
|
Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60));
|
||||||
lwm2mServer.setId(serverId);
|
lwm2mServer.setId(serverId);
|
||||||
initializer.setInstancesForObject(SERVER, lwm2mServer);
|
initializer.setInstancesForObject(SERVER, lwm2mServer);
|
||||||
|
|
||||||
|
|||||||
@ -43,7 +43,6 @@
|
|||||||
"@ngrx/store": "^15.4.0",
|
"@ngrx/store": "^15.4.0",
|
||||||
"@ngrx/store-devtools": "^15.4.0",
|
"@ngrx/store-devtools": "^15.4.0",
|
||||||
"@ngx-translate/core": "^14.0.0",
|
"@ngx-translate/core": "^14.0.0",
|
||||||
"@ngx-translate/http-loader": "^7.0.0",
|
|
||||||
"@svgdotjs/svg.filter.js": "^3.0.8",
|
"@svgdotjs/svg.filter.js": "^3.0.8",
|
||||||
"@svgdotjs/svg.js": "^3.2.0",
|
"@svgdotjs/svg.js": "^3.2.0",
|
||||||
"@tinymce/tinymce-angular": "^7.0.0",
|
"@tinymce/tinymce-angular": "^7.0.0",
|
||||||
|
|||||||
@ -27,10 +27,13 @@ import { LocalStorageService } from '@core/local-storage/local-storage.service';
|
|||||||
import { DomSanitizer } from '@angular/platform-browser';
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
import { MatIconRegistry } from '@angular/material/icon';
|
import { MatIconRegistry } from '@angular/material/icon';
|
||||||
import { combineLatest } from 'rxjs';
|
import { combineLatest } from 'rxjs';
|
||||||
import { selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors';
|
import { getCurrentAuthState, selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors';
|
||||||
import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, skip, tap } from 'rxjs/operators';
|
||||||
import { AuthService } from '@core/auth/auth.service';
|
import { AuthService } from '@core/auth/auth.service';
|
||||||
import { svgIcons, svgIconsUrl } from '@shared/models/icon.models';
|
import { svgIcons, svgIconsUrl } from '@shared/models/icon.models';
|
||||||
|
import { isEqual } from '@core/utils';
|
||||||
|
import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
|
||||||
|
import { SETTINGS_KEY } from '@core/settings/settings.effects';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-root',
|
selector: 'tb-root',
|
||||||
@ -92,8 +95,16 @@ export class AppComponent implements OnInit {
|
|||||||
this.store.pipe(select(selectIsUserLoaded))]
|
this.store.pipe(select(selectIsUserLoaded))]
|
||||||
).pipe(
|
).pipe(
|
||||||
map(results => ({isAuthenticated: results[0], isUserLoaded: results[1]})),
|
map(results => ({isAuthenticated: results[0], isUserLoaded: results[1]})),
|
||||||
distinctUntilChanged(),
|
|
||||||
filter((data) => data.isUserLoaded),
|
filter((data) => data.isUserLoaded),
|
||||||
|
distinctUntilChanged((a, b) => isEqual(a, b)),
|
||||||
|
tap((data) => {
|
||||||
|
let userLang = getCurrentAuthState(this.store).userDetails?.additionalInfo?.lang ?? null;
|
||||||
|
if (!userLang && !data.isAuthenticated) {
|
||||||
|
const settings = this.storageService.getItem(SETTINGS_KEY);
|
||||||
|
userLang = settings?.userLang ?? null;
|
||||||
|
}
|
||||||
|
this.notifyUserLang(userLang);
|
||||||
|
}),
|
||||||
skip(1),
|
skip(1),
|
||||||
).subscribe((data) => {
|
).subscribe((data) => {
|
||||||
this.authService.gotoDefaultPlace(data.isAuthenticated);
|
this.authService.gotoDefaultPlace(data.isAuthenticated);
|
||||||
@ -111,4 +122,8 @@ export class AppComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private notifyUserLang(userLang: string) {
|
||||||
|
this.store.dispatch(new ActionSettingsChangeLanguage({userLang}));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import { Observable, of, ReplaySubject, throwError } from 'rxjs';
|
|||||||
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
|
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models';
|
import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models';
|
||||||
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
|
import { Router, UrlTree } from '@angular/router';
|
||||||
import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from '../http/http-utils';
|
import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from '../http/http-utils';
|
||||||
import { UserService } from '../http/user.service';
|
import { UserService } from '../http/user.service';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
@ -35,7 +35,6 @@ import {
|
|||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { getCurrentAuthState, getCurrentAuthUser } from './auth.selectors';
|
import { getCurrentAuthState, getCurrentAuthUser } from './auth.selectors';
|
||||||
import { Authority } from '@shared/models/authority.enum';
|
import { Authority } from '@shared/models/authority.enum';
|
||||||
import { ActionSettingsChangeLanguage } from '@app/core/settings/settings.actions';
|
|
||||||
import { AuthPayload, AuthState, SysParams, SysParamsState } from '@core/auth/auth.models';
|
import { AuthPayload, AuthState, SysParams, SysParamsState } from '@core/auth/auth.models';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { AuthUser } from '@shared/models/user.model';
|
import { AuthUser } from '@shared/models/user.model';
|
||||||
@ -59,7 +58,6 @@ export class AuthService {
|
|||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private timeService: TimeService,
|
private timeService: TimeService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private route: ActivatedRoute,
|
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private utils: UtilsService,
|
private utils: UtilsService,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
@ -419,14 +417,7 @@ export class AuthService {
|
|||||||
this.loadSystemParams().subscribe(
|
this.loadSystemParams().subscribe(
|
||||||
(sysParams) => {
|
(sysParams) => {
|
||||||
authPayload = {...authPayload, ...sysParams};
|
authPayload = {...authPayload, ...sysParams};
|
||||||
let userLang;
|
|
||||||
if (authPayload.userDetails.additionalInfo && authPayload.userDetails.additionalInfo.lang) {
|
|
||||||
userLang = authPayload.userDetails.additionalInfo.lang;
|
|
||||||
} else {
|
|
||||||
userLang = null;
|
|
||||||
}
|
|
||||||
loadUserSubject.next(authPayload);
|
loadUserSubject.next(authPayload);
|
||||||
this.notifyUserLang(userLang);
|
|
||||||
loadUserSubject.complete();
|
loadUserSubject.complete();
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
@ -607,10 +598,6 @@ export class AuthService {
|
|||||||
this.store.dispatch(new ActionAuthAuthenticated(authPayload));
|
this.store.dispatch(new ActionAuthAuthenticated(authPayload));
|
||||||
}
|
}
|
||||||
|
|
||||||
private notifyUserLang(userLang: string) {
|
|
||||||
this.store.dispatch(new ActionSettingsChangeLanguage({userLang}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateAndValidateToken(token, prefix, notify) {
|
private updateAndValidateToken(token, prefix, notify) {
|
||||||
let valid = false;
|
let valid = false;
|
||||||
const tokenData = this.jwtHelper.decodeToken(token);
|
const tokenData = this.jwtHelper.decodeToken(token);
|
||||||
|
|||||||
@ -31,7 +31,6 @@ import {
|
|||||||
TranslateModule,
|
TranslateModule,
|
||||||
TranslateParser
|
TranslateParser
|
||||||
} from '@ngx-translate/core';
|
} from '@ngx-translate/core';
|
||||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
|
||||||
import { TbMissingTranslationHandler } from './translate/missing-translate-handler';
|
import { TbMissingTranslationHandler } from './translate/missing-translate-handler';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MAT_DIALOG_DEFAULT_OPTIONS, MatDialogConfig, MatDialogModule } from '@angular/material/dialog';
|
import { MAT_DIALOG_DEFAULT_OPTIONS, MatDialogConfig, MatDialogModule } from '@angular/material/dialog';
|
||||||
@ -41,9 +40,10 @@ import { TranslateDefaultCompiler } from '@core/translate/translate-default-comp
|
|||||||
import { WINDOW_PROVIDERS } from '@core/services/window.service';
|
import { WINDOW_PROVIDERS } from '@core/services/window.service';
|
||||||
import { HotkeyModule } from 'angular2-hotkeys';
|
import { HotkeyModule } from 'angular2-hotkeys';
|
||||||
import { TranslateDefaultParser } from '@core/translate/translate-default-parser';
|
import { TranslateDefaultParser } from '@core/translate/translate-default-parser';
|
||||||
|
import { TranslateDefaultLoader } from '@core/translate/translate-default-loader';
|
||||||
|
|
||||||
export function HttpLoaderFactory(http: HttpClient) {
|
export function HttpLoaderFactory(http: HttpClient) {
|
||||||
return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json');
|
return new TranslateDefaultLoader(http);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import { select, Store } from '@ngrx/store';
|
|||||||
import { selectIsAuthenticated } from '@core/auth/auth.selectors';
|
import { selectIsAuthenticated } from '@core/auth/auth.selectors';
|
||||||
import { AppState } from '@core/core.state';
|
import { AppState } from '@core/core.state';
|
||||||
import { map, tap } from 'rxjs/operators';
|
import { map, tap } from 'rxjs/operators';
|
||||||
|
import { RequestConfig } from '@core/http/http-utils';
|
||||||
|
|
||||||
declare const System;
|
declare const System;
|
||||||
|
|
||||||
@ -106,11 +107,11 @@ export class ResourcesService {
|
|||||||
return this.loadResourceByType(fileType, url);
|
return this.loadResourceByType(fileType, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public downloadResource(downloadUrl: string): Observable<any> {
|
public downloadResource(downloadUrl: string, config?: RequestConfig): Observable<any> {
|
||||||
return this.http.get(downloadUrl, {
|
return this.http.get(downloadUrl, {...config, ...{
|
||||||
responseType: 'arraybuffer',
|
responseType: 'arraybuffer',
|
||||||
observe: 'response'
|
observe: 'response'
|
||||||
}).pipe(
|
}}).pipe(
|
||||||
map((response) => {
|
map((response) => {
|
||||||
const headers = response.headers;
|
const headers = response.headers;
|
||||||
const filename = headers.get('x-filename');
|
const filename = headers.get('x-filename');
|
||||||
|
|||||||
@ -28,7 +28,6 @@ import { AppState } from '@app/core/core.state';
|
|||||||
import { LocalStorageService } from '@app/core/local-storage/local-storage.service';
|
import { LocalStorageService } from '@app/core/local-storage/local-storage.service';
|
||||||
import { TitleService } from '@app/core/services/title.service';
|
import { TitleService } from '@app/core/services/title.service';
|
||||||
import { updateUserLang } from '@app/core/settings/settings.utils';
|
import { updateUserLang } from '@app/core/settings/settings.utils';
|
||||||
import { AuthService } from '@core/auth/auth.service';
|
|
||||||
import { UtilsService } from '@core/services/utils.service';
|
import { UtilsService } from '@core/services/utils.service';
|
||||||
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
|
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
|
||||||
import { ActionAuthUpdateLastPublicDashboardId } from '../auth/auth.actions';
|
import { ActionAuthUpdateLastPublicDashboardId } from '../auth/auth.actions';
|
||||||
@ -40,7 +39,6 @@ export class SettingsEffects {
|
|||||||
constructor(
|
constructor(
|
||||||
private actions$: Actions<SettingsActions>,
|
private actions$: Actions<SettingsActions>,
|
||||||
private store: Store<AppState>,
|
private store: Store<AppState>,
|
||||||
private authService: AuthService,
|
|
||||||
private utils: UtilsService,
|
private utils: UtilsService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private localStorageService: LocalStorageService,
|
private localStorageService: LocalStorageService,
|
||||||
@ -49,26 +47,19 @@ export class SettingsEffects {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTranslateServiceLanguage = createEffect(() => this.actions$.pipe(
|
||||||
persistSettings = createEffect(() => this.actions$.pipe(
|
|
||||||
ofType(
|
ofType(
|
||||||
SettingsActionTypes.CHANGE_LANGUAGE,
|
SettingsActionTypes.CHANGE_LANGUAGE,
|
||||||
),
|
),
|
||||||
withLatestFrom(this.store.pipe(select(selectSettingsState))),
|
withLatestFrom(this.store.pipe(select(selectSettingsState))),
|
||||||
tap(([action, settings]) =>
|
map(settings => settings[1]),
|
||||||
this.localStorageService.setItem(SETTINGS_KEY, settings)
|
distinctUntilChanged((a, b) => a?.userLang === b?.userLang),
|
||||||
)
|
tap(setting => {
|
||||||
|
this.localStorageService.setItem(SETTINGS_KEY, setting);
|
||||||
|
updateUserLang(this.translate, setting.userLang);
|
||||||
|
})
|
||||||
), {dispatch: false});
|
), {dispatch: false});
|
||||||
|
|
||||||
|
|
||||||
setTranslateServiceLanguage = createEffect(() => this.store.pipe(
|
|
||||||
select(selectSettingsState),
|
|
||||||
map(settings => settings.userLang),
|
|
||||||
distinctUntilChanged(),
|
|
||||||
tap(userLang => updateUserLang(this.translate, userLang))
|
|
||||||
), {dispatch: false});
|
|
||||||
|
|
||||||
|
|
||||||
setTitle = createEffect(() => merge(
|
setTitle = createEffect(() => merge(
|
||||||
this.actions$.pipe(ofType(SettingsActionTypes.CHANGE_LANGUAGE)),
|
this.actions$.pipe(ofType(SettingsActionTypes.CHANGE_LANGUAGE)),
|
||||||
this.router.events.pipe(filter(event => event instanceof ActivationEnd))
|
this.router.events.pipe(filter(event => event instanceof ActivationEnd))
|
||||||
@ -81,7 +72,6 @@ export class SettingsEffects {
|
|||||||
})
|
})
|
||||||
), {dispatch: false});
|
), {dispatch: false});
|
||||||
|
|
||||||
|
|
||||||
setPublicId = createEffect(() => merge(
|
setPublicId = createEffect(() => merge(
|
||||||
this.router.events.pipe(filter(event => event instanceof ActivationEnd))
|
this.router.events.pipe(filter(event => event instanceof ActivationEnd))
|
||||||
).pipe(
|
).pipe(
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import { environment as env } from '@env/environment';
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import * as _moment from 'moment';
|
import * as _moment from 'moment';
|
||||||
|
|
||||||
export function updateUserLang(translate: TranslateService, userLang: string) {
|
export function updateUserLang(translate: TranslateService, userLang: string, translations = env.supportedLangs) {
|
||||||
let targetLang = userLang;
|
let targetLang = userLang;
|
||||||
if (!env.production) {
|
if (!env.production) {
|
||||||
console.log(`User lang: ${targetLang}`);
|
console.log(`User lang: ${targetLang}`);
|
||||||
@ -29,7 +29,7 @@ export function updateUserLang(translate: TranslateService, userLang: string) {
|
|||||||
console.log(`Fallback to browser lang: ${targetLang}`);
|
console.log(`Fallback to browser lang: ${targetLang}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const detectedSupportedLang = detectSupportedLang(targetLang);
|
const detectedSupportedLang = detectSupportedLang(targetLang, translations);
|
||||||
if (!env.production) {
|
if (!env.production) {
|
||||||
console.log(`Detected supported lang: ${detectedSupportedLang}`);
|
console.log(`Detected supported lang: ${detectedSupportedLang}`);
|
||||||
}
|
}
|
||||||
@ -37,10 +37,10 @@ export function updateUserLang(translate: TranslateService, userLang: string) {
|
|||||||
_moment.locale([detectedSupportedLang]);
|
_moment.locale([detectedSupportedLang]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectSupportedLang(targetLang: string): string {
|
function detectSupportedLang(targetLang: string, translations: string[]): string {
|
||||||
const langTag = (targetLang || '').split('-').join('_');
|
const langTag = (targetLang || '').split('-').join('_');
|
||||||
if (langTag.length) {
|
if (langTag.length) {
|
||||||
if (env.supportedLangs.indexOf(langTag) > -1) {
|
if (translations.indexOf(langTag) > -1) {
|
||||||
return langTag;
|
return langTag;
|
||||||
} else {
|
} else {
|
||||||
const parts = langTag.split('_');
|
const parts = langTag.split('_');
|
||||||
@ -50,7 +50,7 @@ function detectSupportedLang(targetLang: string): string {
|
|||||||
} else {
|
} else {
|
||||||
lang = langTag;
|
lang = langTag;
|
||||||
}
|
}
|
||||||
const foundLangs = env.supportedLangs.filter(
|
const foundLangs = translations.filter(
|
||||||
(supportedLang: string) => {
|
(supportedLang: string) => {
|
||||||
const supportedLangParts = supportedLang.split('_');
|
const supportedLangParts = supportedLang.split('_');
|
||||||
return supportedLangParts[0] === lang;
|
return supportedLangParts[0] === lang;
|
||||||
|
|||||||
30
ui-ngx/src/app/core/translate/translate-default-loader.ts
Normal file
30
ui-ngx/src/app/core/translate/translate-default-loader.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
///
|
||||||
|
/// Copyright © 2016-2024 The Thingsboard Authors
|
||||||
|
///
|
||||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
/// you may not use this file except in compliance with the License.
|
||||||
|
/// You may obtain a copy of the License at
|
||||||
|
///
|
||||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
///
|
||||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
/// See the License for the specific language governing permissions and
|
||||||
|
/// limitations under the License.
|
||||||
|
///
|
||||||
|
|
||||||
|
import { TranslateLoader } from '@ngx-translate/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
|
export class TranslateDefaultLoader implements TranslateLoader {
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getTranslation(lang: string): Observable<object> {
|
||||||
|
return this.http.get(`/assets/locale/locale.constant-${lang}.json`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -338,6 +338,8 @@ export const isEmpty = (a: any): boolean => _.isEmpty(a);
|
|||||||
|
|
||||||
export const unset = (object: any, path: string | symbol): boolean => _.unset(object, path);
|
export const unset = (object: any, path: string | symbol): boolean => _.unset(object, path);
|
||||||
|
|
||||||
|
export const setByPath = <T extends object>(object: T, path: string | number | symbol, value: any): T => _.set(object, path, value);
|
||||||
|
|
||||||
export const isEqualIgnoreUndefined = (a: any, b: any): boolean => {
|
export const isEqualIgnoreUndefined = (a: any, b: any): boolean => {
|
||||||
if (a === b) {
|
if (a === b) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
<input id="username-input" matInput type="email" autofocus formControlName="username" email required/>
|
<input id="username-input" matInput type="email" autofocus formControlName="username" email required/>
|
||||||
<mat-icon matPrefix>email</mat-icon>
|
<mat-icon matPrefix>email</mat-icon>
|
||||||
<mat-error *ngIf="loginFormGroup.get('username').invalid">
|
<mat-error *ngIf="loginFormGroup.get('username').invalid">
|
||||||
{{ 'user.invalid-email-format' | translate }}
|
{{ 'login.invalid-email-format' | translate }}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="tb-appearance-transparent">
|
<mat-form-field class="tb-appearance-transparent">
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
<div class="tb-json-content" style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
|
<div class="tb-json-content" style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
|
||||||
tb-fullscreen
|
tb-fullscreen
|
||||||
[fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
|
[fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
|
||||||
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-json-content-toolbar">
|
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-json-content-toolbar" *ngIf="hideToolbar">
|
||||||
<label class="tb-title no-padding" [ngClass]="{'tb-error': !disabled && (!contentValid || required && !contentBody), 'tb-required': !disabled && required}">{{ label }}</label>
|
<label class="tb-title no-padding" [ngClass]="{'tb-error': !disabled && (!contentValid || required && !contentBody), 'tb-required': !disabled && required}">{{ label }}</label>
|
||||||
<span fxFlex></span>
|
<span fxFlex></span>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
|
|||||||
@ -18,17 +18,19 @@ import {
|
|||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
|
Output,
|
||||||
SimpleChanges,
|
SimpleChanges,
|
||||||
ViewChild, ViewEncapsulation
|
ViewChild,
|
||||||
|
ViewEncapsulation
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ControlValueAccessor, UntypedFormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
|
import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormControl, Validator } from '@angular/forms';
|
||||||
import { Ace } from 'ace-builds';
|
import { Ace } from 'ace-builds';
|
||||||
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
|
||||||
import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
|
import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppState } from '@core/core.state';
|
import { AppState } from '@core/core.state';
|
||||||
@ -38,6 +40,7 @@ import { guid } from '@core/utils';
|
|||||||
import { ResizeObserver } from '@juggle/resize-observer';
|
import { ResizeObserver } from '@juggle/resize-observer';
|
||||||
import { getAce } from '@shared/models/ace/ace.models';
|
import { getAce } from '@shared/models/ace/ace.models';
|
||||||
import { beautifyJs } from '@shared/models/beautify.models';
|
import { beautifyJs } from '@shared/models/beautify.models';
|
||||||
|
import { coerceBoolean } from '@shared/decorators/coercion';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-json-content',
|
selector: 'tb-json-content',
|
||||||
@ -79,41 +82,30 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
|
|||||||
|
|
||||||
@Input() editorStyle: {[klass: string]: any};
|
@Input() editorStyle: {[klass: string]: any};
|
||||||
|
|
||||||
private readonlyValue: boolean;
|
@Input() tbPlaceholder: string;
|
||||||
get readonly(): boolean {
|
|
||||||
return this.readonlyValue;
|
|
||||||
}
|
|
||||||
@Input()
|
|
||||||
set readonly(value: boolean) {
|
|
||||||
this.readonlyValue = coerceBooleanProperty(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private validateContentValue: boolean;
|
|
||||||
get validateContent(): boolean {
|
|
||||||
return this.validateContentValue;
|
|
||||||
}
|
|
||||||
@Input()
|
@Input()
|
||||||
set validateContent(value: boolean) {
|
@coerceBoolean()
|
||||||
this.validateContentValue = coerceBooleanProperty(value);
|
hideToolbar = false;
|
||||||
}
|
|
||||||
|
|
||||||
private validateOnChangeValue: boolean;
|
|
||||||
get validateOnChange(): boolean {
|
|
||||||
return this.validateOnChangeValue;
|
|
||||||
}
|
|
||||||
@Input()
|
@Input()
|
||||||
set validateOnChange(value: boolean) {
|
@coerceBoolean()
|
||||||
this.validateOnChangeValue = coerceBooleanProperty(value);
|
readonly: boolean;
|
||||||
}
|
|
||||||
|
|
||||||
private requiredValue: boolean;
|
|
||||||
get required(): boolean {
|
|
||||||
return this.requiredValue;
|
|
||||||
}
|
|
||||||
@Input()
|
@Input()
|
||||||
set required(value: boolean) {
|
@coerceBoolean()
|
||||||
this.requiredValue = coerceBooleanProperty(value);
|
validateContent: boolean;
|
||||||
}
|
|
||||||
|
@Input()
|
||||||
|
@coerceBoolean()
|
||||||
|
validateOnChange: boolean;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
@coerceBoolean()
|
||||||
|
required: boolean;
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
blur: EventEmitter<void> = new EventEmitter<void>();
|
||||||
|
|
||||||
fullscreen = false;
|
fullscreen = false;
|
||||||
|
|
||||||
@ -124,6 +116,7 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
|
|||||||
errorShowed = false;
|
errorShowed = false;
|
||||||
|
|
||||||
private propagateChange = null;
|
private propagateChange = null;
|
||||||
|
private onTouched = () => {};
|
||||||
|
|
||||||
constructor(public elementRef: ElementRef,
|
constructor(public elementRef: ElementRef,
|
||||||
protected store: Store<AppState>,
|
protected store: Store<AppState>,
|
||||||
@ -163,11 +156,17 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
|
|||||||
this.updateView();
|
this.updateView();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (this.validateContent) {
|
|
||||||
this.jsonEditor.on('blur', () => {
|
this.jsonEditor.on('blur', () => {
|
||||||
|
if (this.validateContent) {
|
||||||
this.contentValid = this.doValidate(true);
|
this.contentValid = this.doValidate(true);
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
this.onTouched();
|
||||||
|
this.blur.next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.tbPlaceholder && this.tbPlaceholder.length) {
|
||||||
|
this.createPlaceholder();
|
||||||
}
|
}
|
||||||
this.editorResize$ = new ResizeObserver(() => {
|
this.editorResize$ = new ResizeObserver(() => {
|
||||||
this.onAceEditorResize();
|
this.onAceEditorResize();
|
||||||
@ -177,6 +176,39 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createPlaceholder() {
|
||||||
|
this.jsonEditor.on('input', this.updateEditorPlaceholder.bind(this));
|
||||||
|
setTimeout(this.updateEditorPlaceholder.bind(this), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateEditorPlaceholder() {
|
||||||
|
const shouldShow = !this.jsonEditor.session.getValue().length;
|
||||||
|
let node: HTMLElement = (this.jsonEditor.renderer as any).emptyMessageNode;
|
||||||
|
if (!shouldShow && node) {
|
||||||
|
this.jsonEditor.renderer.getMouseEventTarget().removeChild(node);
|
||||||
|
(this.jsonEditor.renderer as any).emptyMessageNode = null;
|
||||||
|
} else if (shouldShow && !node) {
|
||||||
|
const placeholderElement = $('<textarea></textarea>');
|
||||||
|
placeholderElement.text(this.tbPlaceholder);
|
||||||
|
placeholderElement.addClass('ace_invisible ace_emptyMessage');
|
||||||
|
placeholderElement.css({
|
||||||
|
padding: '0 9px',
|
||||||
|
width: '100%',
|
||||||
|
border: 'none',
|
||||||
|
textWrap: 'nowrap',
|
||||||
|
whiteSpace: 'pre',
|
||||||
|
overflow: 'hidden',
|
||||||
|
resize: 'none',
|
||||||
|
fontSize: '15px'
|
||||||
|
});
|
||||||
|
const rows = this.tbPlaceholder.split('\n').length;
|
||||||
|
placeholderElement.attr('rows', rows);
|
||||||
|
node = placeholderElement[0];
|
||||||
|
(this.jsonEditor.renderer as any).emptyMessageNode = node;
|
||||||
|
this.jsonEditor.renderer.getMouseEventTarget().appendChild(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
if (this.editorResize$) {
|
if (this.editorResize$) {
|
||||||
this.editorResize$.disconnect();
|
this.editorResize$.disconnect();
|
||||||
@ -219,6 +251,7 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
|
|||||||
}
|
}
|
||||||
|
|
||||||
registerOnTouched(fn: any): void {
|
registerOnTouched(fn: any): void {
|
||||||
|
this.onTouched = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
setDisabledState(isDisabled: boolean): void {
|
setDisabledState(isDisabled: boolean): void {
|
||||||
|
|||||||
8263
ui-ngx/src/assets/locale/locale.constant-ar_AR.json
Normal file
8263
ui-ngx/src/assets/locale/locale.constant-ar_AR.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -3311,6 +3311,7 @@
|
|||||||
"new-password-again": "Confirm new password",
|
"new-password-again": "Confirm new password",
|
||||||
"password-link-sent-message": "Reset link has been sent",
|
"password-link-sent-message": "Reset link has been sent",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"invalid-email-format": "Invalid email format.",
|
||||||
"login-with": "Login with {{name}}",
|
"login-with": "Login with {{name}}",
|
||||||
"or": "or",
|
"or": "or",
|
||||||
"error": "Login error",
|
"error": "Login error",
|
||||||
@ -7290,6 +7291,7 @@
|
|||||||
"language": {
|
"language": {
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"locales": {
|
"locales": {
|
||||||
|
"ar_AR": "اَلْعَرَبِيَّةُ",
|
||||||
"ca_ES": "Catalan",
|
"ca_ES": "Catalan",
|
||||||
"cs_CZ": "Česky",
|
"cs_CZ": "Česky",
|
||||||
"da_DK": "Dansk",
|
"da_DK": "Dansk",
|
||||||
|
|||||||
@ -2572,13 +2572,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.3.0"
|
tslib "^2.3.0"
|
||||||
|
|
||||||
"@ngx-translate/http-loader@^7.0.0":
|
|
||||||
version "7.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@ngx-translate/http-loader/-/http-loader-7.0.0.tgz#905f38d8d13342621516635bf480ff9a4f73e9fc"
|
|
||||||
integrity sha512-j+NpXXlcGVdyUNyY/qsJrqqeAdJdizCd+GKh3usXExSqy1aE9866jlAIL+xrfDU4w+LiMoma5pgE4emvFebZmA==
|
|
||||||
dependencies:
|
|
||||||
tslib "^2.3.0"
|
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user