diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index ac6c5d2f4e..0eab7d353c 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -86,7 +86,7 @@ } }, "serve": { - "builder": "@angular-builders/dev-server:generic", + "builder": "@angular-builders/custom-webpack:dev-server", "options": { "browserTarget": "thingsboard:build", "proxyConfig": "proxy.conf.json" diff --git a/ui-ngx/package-lock.json b/ui-ngx/package-lock.json index 37ca17e5fc..5500e96dfb 100644 --- a/ui-ngx/package-lock.json +++ b/ui-ngx/package-lock.json @@ -14,12 +14,6 @@ "webpack-merge": "^4.2.1" } }, - "@angular-builders/dev-server": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@angular-builders/dev-server/-/dev-server-7.3.1.tgz", - "integrity": "sha512-rFr0NyFcwTb4RkkboYQN5JeR9ZraOkfUrQYljMSe/O01MM3SJvE8LYJbsyMwGtp71Rc8T6JrpdxaNEeYCV/4PA==", - "dev": true - }, "@angular-devkit/architect": { "version": "0.802.0", "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.802.0.tgz", diff --git a/ui-ngx/package.json b/ui-ngx/package.json index c3fe04638d..378f6e3f6d 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -47,7 +47,6 @@ }, "devDependencies": { "@angular-builders/custom-webpack": "^8.1.0", - "@angular-builders/dev-server": "^7.3.1", "@angular-devkit/build-angular": "^0.802.0", "@angular/cli": "~8.2.0", "@angular/compiler-cli": "~8.2.0", diff --git a/ui-ngx/src/app/app-routing.module.ts b/ui-ngx/src/app/app-routing.module.ts new file mode 100644 index 0000000000..02f99e1c56 --- /dev/null +++ b/ui-ngx/src/app/app-routing.module.ts @@ -0,0 +1,36 @@ +/// +/// Copyright © 2016-2019 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 { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +const routes: Routes = [ + { path: '', + redirectTo: 'home', + pathMatch: 'full', + data: { + breadcrumb: { + skip: true + } + } + } +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class AppRoutingModule { } diff --git a/ui-ngx/src/app/app.component.html b/ui-ngx/src/app/app.component.html new file mode 100644 index 0000000000..0cd4586f74 --- /dev/null +++ b/ui-ngx/src/app/app.component.html @@ -0,0 +1,20 @@ + + + + diff --git a/ui-ngx/src/app/app.component.scss b/ui-ngx/src/app/app.component.scss new file mode 100644 index 0000000000..e2b008f382 --- /dev/null +++ b/ui-ngx/src/app/app.component.scss @@ -0,0 +1,15 @@ +/** + * Copyright © 2016-2019 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. + */ diff --git a/ui-ngx/src/app/app.component.ts b/ui-ngx/src/app/app.component.ts new file mode 100644 index 0000000000..19a06c3d0b --- /dev/null +++ b/ui-ngx/src/app/app.component.ts @@ -0,0 +1,62 @@ +/// +/// Copyright © 2016-2019 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 { Component, OnInit } from '@angular/core'; + +import { environment as env } from '@env/environment'; + +import { TranslateService } from '@ngx-translate/core'; +import { Store } from '@ngrx/store'; +import { AppState } from './core/core.state'; +import { LocalStorageService } from './core/local-storage/local-storage.service'; +import { DomSanitizer } from '@angular/platform-browser'; +import { MatIconRegistry } from '@angular/material'; + +@Component({ + selector: 'tb-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent implements OnInit { + + constructor(private store: Store, + private storageService: LocalStorageService, + private translate: TranslateService, + private matIconRegistry: MatIconRegistry, + private domSanitizer: DomSanitizer) { + + console.log(`ThingsBoard Version: ${env.tbVersion}`); + + this.matIconRegistry.addSvgIconSetInNamespace('mdi', + this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/mdi.svg')); + + this.storageService.testLocalStorage(); + + this.setupTranslate(); + } + + setupTranslate() { + console.log(`Supported Langs: ${env.supportedLangs}`); + this.translate.addLangs(env.supportedLangs); + console.log(`Default Lang: ${env.defaultLang}`); + this.translate.setDefaultLang(env.defaultLang); + } + + ngOnInit() { + } + +} + diff --git a/ui-ngx/src/app/app.module.ts b/ui-ngx/src/app/app.module.ts index aa76e9b7a0..1a1e6ab20b 100644 --- a/ui-ngx/src/app/app.module.ts +++ b/ui-ngx/src/app/app.module.ts @@ -18,26 +18,26 @@ import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NgModule } from '@angular/core'; -/* import { AppRoutingModule } from './app-routing.module'; +import { AppRoutingModule } from './app-routing.module'; import { CoreModule } from './core/core.module'; import { LoginModule } from './modules/login/login.module'; import { HomeModule } from './modules/home/home.module'; -import { AppComponent } from './app.component'; */ +import { AppComponent } from './app.component'; @NgModule({ declarations: [ - /* AppComponent */ + AppComponent ], imports: [ - /* BrowserModule, + BrowserModule, BrowserAnimationsModule, AppRoutingModule, CoreModule, LoginModule, - HomeModule */ + HomeModule ], providers: [], - bootstrap: [/*AppComponent*/] + bootstrap: [AppComponent] }) export class AppModule { } diff --git a/ui-ngx/src/app/core/auth/auth.actions.ts b/ui-ngx/src/app/core/auth/auth.actions.ts new file mode 100644 index 0000000000..d5a30c0856 --- /dev/null +++ b/ui-ngx/src/app/core/auth/auth.actions.ts @@ -0,0 +1,50 @@ +/// +/// Copyright © 2016-2019 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 { Action } from '@ngrx/store'; +import { AuthUser, User } from '../../shared/models/user.model'; +import { AuthPayload } from '@core/auth/auth.models'; + +export enum AuthActionTypes { + AUTHENTICATED = '[Auth] Authenticated', + UNAUTHENTICATED = '[Auth] Unauthenticated', + LOAD_USER = '[Auth] Load User', + UPDATE_USER_DETAILS = '[Auth] Update User Details' +} + +export class ActionAuthAuthenticated implements Action { + readonly type = AuthActionTypes.AUTHENTICATED; + + constructor(readonly payload: AuthPayload) {} +} + +export class ActionAuthUnauthenticated implements Action { + readonly type = AuthActionTypes.UNAUTHENTICATED; +} + +export class ActionAuthLoadUser implements Action { + readonly type = AuthActionTypes.LOAD_USER; + + constructor(readonly payload: { isUserLoaded: boolean }) {} +} + +export class ActionAuthUpdateUserDetails implements Action { + readonly type = AuthActionTypes.UPDATE_USER_DETAILS; + + constructor(readonly payload: { userDetails: User }) {} +} + +export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated | ActionAuthLoadUser | ActionAuthUpdateUserDetails; diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts new file mode 100644 index 0000000000..21daba10ea --- /dev/null +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -0,0 +1,31 @@ +/// +/// Copyright © 2016-2019 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 { AuthUser, User } from '../../shared/models/user.model'; + +export interface AuthPayload { + authUser: AuthUser; + userDetails: User; + userTokenAccessEnabled: boolean; +} + +export interface AuthState { + isAuthenticated: boolean; + isUserLoaded: boolean; + authUser: AuthUser; + userDetails: User; + userTokenAccessEnabled: boolean; +} diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts new file mode 100644 index 0000000000..b82f76f9c4 --- /dev/null +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -0,0 +1,53 @@ +/// +/// Copyright © 2016-2019 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 { AuthPayload, AuthState } from './auth.models'; +import { AuthActions, AuthActionTypes } from './auth.actions'; + +const emptyUserAuthState: AuthPayload = { + authUser: null, + userDetails: null, + userTokenAccessEnabled: false +}; + +export const initialState: AuthState = { + isAuthenticated: false, + isUserLoaded: false, + ...emptyUserAuthState +}; + +export function authReducer( + state: AuthState = initialState, + action: AuthActions +): AuthState { + switch (action.type) { + case AuthActionTypes.AUTHENTICATED: + return { ...state, isAuthenticated: true, ...action.payload }; + + case AuthActionTypes.UNAUTHENTICATED: + return { ...state, isAuthenticated: false, ...emptyUserAuthState }; + + case AuthActionTypes.LOAD_USER: + return { ...state, ...action.payload, isAuthenticated: action.payload.isUserLoaded ? state.isAuthenticated : false, + ...action.payload.isUserLoaded ? {} : emptyUserAuthState }; + + case AuthActionTypes.UPDATE_USER_DETAILS: + return { ...state, ...action.payload}; + + default: + return state; + } +} diff --git a/ui-ngx/src/app/core/auth/auth.selectors.ts b/ui-ngx/src/app/core/auth/auth.selectors.ts new file mode 100644 index 0000000000..7e024d333f --- /dev/null +++ b/ui-ngx/src/app/core/auth/auth.selectors.ts @@ -0,0 +1,72 @@ +/// +/// Copyright © 2016-2019 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 { createFeatureSelector, createSelector, select, Store } from '@ngrx/store'; + +import { AppState } from '../core.state'; +import { AuthState } from './auth.models'; +import { take } from 'rxjs/operators'; +import { AuthUser } from '@shared/models/user.model'; + +export const selectAuthState = createFeatureSelector( + 'auth' +); + +export const selectAuth = createSelector( + selectAuthState, + (state: AuthState) => state +); + +export const selectIsAuthenticated = createSelector( + selectAuthState, + (state: AuthState) => state.isAuthenticated +); + +export const selectIsUserLoaded = createSelector( + selectAuthState, + (state: AuthState) => state.isUserLoaded +); + +export const selectAuthUser = createSelector( + selectAuthState, + (state: AuthState) => state.authUser +); + +export const selectUserDetails = createSelector( + selectAuthState, + (state: AuthState) => state.userDetails +); + +export const selectUserTokenAccessEnabled = createSelector( + selectAuthState, + (state: AuthState) => state.userTokenAccessEnabled +); + +export function getCurrentAuthState(store: Store): AuthState { + let state: AuthState; + store.pipe(select(selectAuth), take(1)).subscribe( + val => state = val + ); + return state; +} + +export function getCurrentAuthUser(store: Store): AuthUser { + let authUser: AuthUser; + store.pipe(select(selectAuthUser), take(1)).subscribe( + val => authUser = val + ); + return authUser; +} diff --git a/ui-ngx/src/app/core/auth/auth.service.spec.ts b/ui-ngx/src/app/core/auth/auth.service.spec.ts new file mode 100644 index 0000000000..e3168f0560 --- /dev/null +++ b/ui-ngx/src/app/core/auth/auth.service.spec.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2019 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 { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: AuthService = TestBed.get(AuthService); + expect(service).toBeTruthy(); + }); +}); diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts new file mode 100644 index 0000000000..2df91a6416 --- /dev/null +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -0,0 +1,403 @@ +/// +/// Copyright © 2016-2019 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 {Injectable, NgZone} from '@angular/core'; +import {JwtHelperService} from '@auth0/angular-jwt'; +import {HttpClient} from '@angular/common/http'; + +import {combineLatest, forkJoin, Observable, of} from 'rxjs'; +import {distinctUntilChanged, filter, map, skip, tap} from 'rxjs/operators'; + +import {LoginRequest, LoginResponse} from '../../shared/models/login.models'; +import {ActivatedRoute, Router, UrlTree} from '@angular/router'; +import {defaultHttpOptions} from '../http/http-utils'; +import {ReplaySubject} from 'rxjs/internal/ReplaySubject'; +import {UserService} from '../http/user.service'; +import {select, Store} from '@ngrx/store'; +import {AppState} from '../core.state'; +import {ActionAuthAuthenticated, ActionAuthLoadUser, ActionAuthUnauthenticated} from './auth.actions'; +import {getCurrentAuthUser, selectIsAuthenticated, selectIsUserLoaded} from './auth.selectors'; +import {Authority} from '../../shared/models/authority.enum'; +import {ActionSettingsChangeLanguage} from '@app/core/settings/settings.actions'; +import {AuthPayload} from '@core/auth/auth.models'; +import {TranslateService} from '@ngx-translate/core'; +import {AuthUser} from '@shared/models/user.model'; +import {TimeService} from '@core/services/time.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + + constructor( + private store: Store, + private http: HttpClient, + private userService: UserService, + private timeService: TimeService, + private router: Router, + private route: ActivatedRoute, + private zone: NgZone, + private translate: TranslateService + ) { + combineLatest( + this.store.pipe(select(selectIsAuthenticated)), + this.store.pipe(select(selectIsUserLoaded)) + ).pipe( + map(results => ({isAuthenticated: results[0], isUserLoaded: results[1]})), + distinctUntilChanged(), + filter((data) => data.isUserLoaded ), + skip(1), + ).subscribe((data) => { + this.gotoDefaultPlace(data.isAuthenticated); + }); + this.reloadUser(); + } + + redirectUrl: string; + + private refreshTokenSubject: ReplaySubject = null; + private jwtHelper = new JwtHelperService(); + + private static _storeGet(key) { + return localStorage.getItem(key); + } + + private static isTokenValid(prefix) { + const clientExpiration = AuthService._storeGet(prefix + '_expiration'); + return clientExpiration && Number(clientExpiration) > (new Date().valueOf() + 2000); + } + + public static isJwtTokenValid() { + return AuthService.isTokenValid('jwt_token'); + } + + private static clearTokenData() { + localStorage.removeItem('jwt_token'); + localStorage.removeItem('jwt_token_expiration'); + localStorage.removeItem('refresh_token'); + localStorage.removeItem('refresh_token_expiration'); + } + + public static getJwtToken() { + return AuthService._storeGet('jwt_token'); + } + + public reloadUser() { + this.loadUser(true).subscribe( + (authPayload) => { + this.notifyAuthenticated(authPayload); + this.notifyUserLoaded(true); + }, + () => { + this.notifyUnauthenticated(); + this.notifyUserLoaded(true); + } + ); + } + + + public login(loginRequest: LoginRequest): Observable { + return this.http.post('/api/auth/login', loginRequest, defaultHttpOptions()).pipe( + tap((loginResponse: LoginResponse) => { + this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); + } + )); + } + + public sendResetPasswordLink(email: string) { + return this.http.post('/api/noauth/resetPasswordByEmail', + {email}, defaultHttpOptions()); + } + + public activate(activateToken: string, password: string): Observable { + return this.http.post('/api/noauth/activate', {activateToken, password}, defaultHttpOptions()).pipe( + tap((loginResponse: LoginResponse) => { + this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); + } + )); + } + + public resetPassword(resetToken: string, password: string): Observable { + return this.http.post('/api/noauth/resetPassword', {resetToken, password}, defaultHttpOptions()).pipe( + tap((loginResponse: LoginResponse) => { + this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); + } + )); + } + + public changePassword(currentPassword: string, newPassword: string) { + return this.http.post('/api/auth/changePassword', + {currentPassword, newPassword}, defaultHttpOptions()); + } + + public activateByEmailCode(emailCode: string): Observable { + return this.http.post(`/api/noauth/activateByEmailCode?emailCode=${emailCode}`, + null, defaultHttpOptions()); + } + + public resendEmailActivation(email: string) { + return this.http.post(`/api/noauth/resendEmailActivation?email=${email}`, + null, defaultHttpOptions()); + } + + public loginAsUser(userId: string) { + return this.http.get(`/api/user/${userId}/token`, defaultHttpOptions()).pipe( + tap((loginResponse: LoginResponse) => { + this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); + } + )); + } + + public logout(captureLastUrl: boolean = false) { + if (captureLastUrl) { + this.redirectUrl = this.router.url; + } + this.clearJwtToken(); + } + + private notifyUserLoaded(isUserLoaded: boolean) { + this.store.dispatch(new ActionAuthLoadUser({isUserLoaded})); + } + + public gotoDefaultPlace(isAuthenticated: boolean) { + const url = this.defaultUrl(isAuthenticated); + this.zone.run(() => { + this.router.navigateByUrl(url); + }); + } + + public defaultUrl(isAuthenticated: boolean): UrlTree { + if (isAuthenticated) { + if (this.redirectUrl) { + const redirectUrl = this.redirectUrl; + this.redirectUrl = null; + return this.router.parseUrl(redirectUrl); + } else { + + // TODO: + + return this.router.parseUrl('home'); + } + } else { + return this.router.parseUrl('login'); + } + } + + private loadUser(doTokenRefresh): Observable { + const authUser = getCurrentAuthUser(this.store); + if (!authUser) { + return this.procceedJwtTokenValidate(doTokenRefresh); + } else { + return of({} as AuthPayload); + } + } + + private procceedJwtTokenValidate(doTokenRefresh: boolean): Observable { + const loadUserSubject = new ReplaySubject(); + this.validateJwtToken(doTokenRefresh).subscribe( + () => { + let authPayload = {} as AuthPayload; + const jwtToken = AuthService._storeGet('jwt_token'); + authPayload.authUser = this.jwtHelper.decodeToken(jwtToken); + if (authPayload.authUser && authPayload.authUser.scopes && authPayload.authUser.scopes.length) { + authPayload.authUser.authority = Authority[authPayload.authUser.scopes[0]]; + } else if (authPayload.authUser) { + authPayload.authUser.authority = Authority.ANONYMOUS; + } + const sysParamsObservable = this.loadSystemParams(authPayload.authUser); + if (authPayload.authUser.isPublic) { + + // TODO: + + } else if (authPayload.authUser.userId) { + this.userService.getUser(authPayload.authUser.userId).subscribe( + (user) => { + sysParamsObservable.subscribe( + (sysParams) => { + authPayload = {...authPayload, ...sysParams}; + authPayload.userDetails = user; + let userLang; + if (authPayload.userDetails.additionalInfo && authPayload.userDetails.additionalInfo.lang) { + userLang = authPayload.userDetails.additionalInfo.lang; + } else { + userLang = null; + } + this.notifyUserLang(userLang); + loadUserSubject.next(authPayload); + loadUserSubject.complete(); + }, + (err) => { + loadUserSubject.error(err); + this.logout(); + }); + }, + (err) => { + loadUserSubject.error(err); + this.logout(); + } + ); + } else { + loadUserSubject.error(null); + } + }, + (err) => { + loadUserSubject.error(err); + } + ); + return loadUserSubject; + } + + private loadIsUserTokenAccessEnabled(authUser: AuthUser): Observable { + if (authUser.authority === Authority.SYS_ADMIN || + authUser.authority === Authority.TENANT_ADMIN) { + return this.http.get('/api/user/tokenAccessEnabled', defaultHttpOptions()); + } else { + return of(false); + } + } + + private loadSystemParams(authUser: AuthUser): Observable { + const sources: Array> = [this.loadIsUserTokenAccessEnabled(authUser), + this.timeService.loadMaxDatapointsLimit()]; + return forkJoin(sources) + .pipe(map((data) => { + const userTokenAccessEnabled: boolean = data[0]; + return {userTokenAccessEnabled}; + })); + } + + public refreshJwtToken(): Observable { + let response: Observable = this.refreshTokenSubject; + if (this.refreshTokenSubject === null) { + this.refreshTokenSubject = new ReplaySubject(1); + response = this.refreshTokenSubject; + const refreshToken = AuthService._storeGet('refresh_token'); + const refreshTokenValid = AuthService.isTokenValid('refresh_token'); + this.setUserFromJwtToken(null, null, false); + if (!refreshTokenValid) { + this.refreshTokenSubject.error(new Error(this.translate.instant('access.refresh-token-expired'))); + this.refreshTokenSubject = null; + } else { + const refreshTokenRequest = { + refreshToken + }; + const refreshObservable = this.http.post('/api/auth/token', refreshTokenRequest, defaultHttpOptions()); + refreshObservable.subscribe((loginResponse: LoginResponse) => { + this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, false); + this.refreshTokenSubject.next(loginResponse); + this.refreshTokenSubject.complete(); + this.refreshTokenSubject = null; + }, () => { + this.clearJwtToken(); + this.refreshTokenSubject.error(new Error(this.translate.instant('access.refresh-token-failed'))); + this.refreshTokenSubject = null; + }); + } + } + return response; + } + + private validateJwtToken(doRefresh): Observable { + const subject = new ReplaySubject(); + if (!AuthService.isTokenValid('jwt_token')) { + if (doRefresh) { + this.refreshJwtToken().subscribe( + () => { + subject.next(); + subject.complete(); + }, + (err) => { + subject.error(err); + } + ); + } else { + this.clearJwtToken(); + subject.error(null); + } + } else { + subject.next(); + subject.complete(); + } + return subject; + } + + public refreshTokenPending() { + return this.refreshTokenSubject !== null; + } + + public setUserFromJwtToken(jwtToken, refreshToken, notify) { + if (!jwtToken) { + AuthService.clearTokenData(); + if (notify) { + this.notifyUnauthenticated(); + } + } else { + this.updateAndValidateToken(jwtToken, 'jwt_token', true); + this.updateAndValidateToken(refreshToken, 'refresh_token', true); + if (notify) { + this.notifyUserLoaded(false); + this.loadUser(false).subscribe( + (authPayload) => { + this.notifyUserLoaded(true); + this.notifyAuthenticated(authPayload); + }, + () => { + this.notifyUserLoaded(true); + this.notifyUnauthenticated(); + } + ); + } else { + this.loadUser(false); + } + } + } + + private notifyUnauthenticated() { + this.store.dispatch(new ActionAuthUnauthenticated()); + } + + private notifyAuthenticated(authPayload: AuthPayload) { + this.store.dispatch(new ActionAuthAuthenticated(authPayload)); + } + + private notifyUserLang(userLang: string) { + this.store.dispatch(new ActionSettingsChangeLanguage({userLang})); + } + + private updateAndValidateToken(token, prefix, notify) { + let valid = false; + const tokenData = this.jwtHelper.decodeToken(token); + const issuedAt = tokenData.iat; + const expTime = tokenData.exp; + if (issuedAt && expTime) { + const ttl = expTime - issuedAt; + if (ttl > 0) { + const clientExpiration = new Date().valueOf() + ttl * 1000; + localStorage.setItem(prefix, token); + localStorage.setItem(prefix + '_expiration', '' + clientExpiration); + valid = true; + } + } + if (!valid && notify) { + this.notifyUnauthenticated(); + } + } + + private clearJwtToken() { + this.setUserFromJwtToken(null, null, true); + } + +} diff --git a/ui-ngx/src/app/core/core.module.ts b/ui-ngx/src/app/core/core.module.ts new file mode 100644 index 0000000000..d07c7e7383 --- /dev/null +++ b/ui-ngx/src/app/core/core.module.ts @@ -0,0 +1,100 @@ +/// +/// Copyright © 2016-2019 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http'; +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; +import { StoreDevtoolsModule } from '@ngrx/store-devtools'; +import { GlobalHttpInterceptor } from './interceptors/global-http-interceptor'; +import { effects, metaReducers, reducers } from './core.state'; +import { environment as env } from '@env/environment'; + +import { + MissingTranslationHandler, + TranslateCompiler, + TranslateLoader, + TranslateModule +} from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +import { TbMissingTranslationHandler } from './translate/missing-translate-handler'; +import { MatButtonModule, MatDialogModule, MatSnackBarModule } from '@angular/material'; +import { ConfirmDialogComponent } from '@core/services/dialog/confirm-dialog.component'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { TranslateDefaultCompiler } from '@core/translate/translate-default-compiler'; +import { AlertDialogComponent } from '@core/services/dialog/alert-dialog.component'; +import { WINDOW_PROVIDERS } from '@core/services/window.service'; + +export function HttpLoaderFactory(http: HttpClient) { + return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json'); +} + +@NgModule({ + entryComponents: [ + ConfirmDialogComponent, + AlertDialogComponent + ], + declarations: [ + ConfirmDialogComponent, + AlertDialogComponent + ], + imports: [ + CommonModule, + HttpClientModule, + FlexLayoutModule.withConfig({addFlexToParent: false}), + MatDialogModule, + MatButtonModule, + MatSnackBarModule, + + // ngx-translate + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + }, + missingTranslationHandler: { + provide: MissingTranslationHandler, + useClass: TbMissingTranslationHandler + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateDefaultCompiler + } + }), + + // ngrx + StoreModule.forRoot(reducers, { metaReducers }), + EffectsModule.forRoot(effects), + env.production + ? [] + : StoreDevtoolsModule.instrument({ + name: env.appTitle + }) + ], + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: GlobalHttpInterceptor, + multi: true + }, + WINDOW_PROVIDERS + ], + exports: [] +}) +export class CoreModule { +} diff --git a/ui-ngx/src/app/core/core.state.ts b/ui-ngx/src/app/core/core.state.ts new file mode 100644 index 0000000000..4057216083 --- /dev/null +++ b/ui-ngx/src/app/core/core.state.ts @@ -0,0 +1,65 @@ +/// +/// Copyright © 2016-2019 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 { + ActionReducerMap, + MetaReducer, Store +} from '@ngrx/store'; +import { storeFreeze } from 'ngrx-store-freeze'; + +import { environment as env} from '@env/environment'; + +import { initStateFromLocalStorage } from './meta-reducers/init-state-from-local-storage.reducer'; +import { debug } from './meta-reducers/debug.reducer'; +import { LoadState } from './interceptors/load.models'; +import { loadReducer } from './interceptors/load.reducer'; +import { AuthState } from './auth/auth.models'; +import { authReducer } from './auth/auth.reducer'; +import { settingsReducer } from '@app/core/settings/settings.reducer'; +import { SettingsState } from '@app/core/settings/settings.models'; +import { Type } from '@angular/core'; +import { SettingsEffects } from '@app/core/settings/settings.effects'; +import { NotificationState } from '@app/core/notification/notification.models'; +import { notificationReducer } from '@app/core/notification/notification.reducer'; +import { NotificationEffects } from '@app/core/notification/notification.effects'; +import { take } from 'rxjs/operators'; + +export const reducers: ActionReducerMap = { + load: loadReducer, + auth: authReducer, + settings: settingsReducer, + notification: notificationReducer +}; + +export const metaReducers: MetaReducer[] = [ + initStateFromLocalStorage +]; +if (!env.production) { + metaReducers.unshift(storeFreeze); + metaReducers.unshift(debug); +} + +export const effects: Type[] = [ + SettingsEffects, + NotificationEffects +]; + +export interface AppState { + load: LoadState; + auth: AuthState; + settings: SettingsState; + notification: NotificationState; +} diff --git a/ui-ngx/src/app/core/guards/auth.guard.ts b/ui-ngx/src/app/core/guards/auth.guard.ts new file mode 100644 index 0000000000..4ba3ca7612 --- /dev/null +++ b/ui-ngx/src/app/core/guards/auth.guard.ts @@ -0,0 +1,113 @@ +/// +/// Copyright © 2016-2019 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 { Injectable, NgZone } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivate, + CanActivateChild, + RouterStateSnapshot +} from '@angular/router'; +import { AuthService } from '../auth/auth.service'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '../core.state'; +import { selectAuth } from '../auth/auth.selectors'; +import { catchError, map, skipWhile, take } from 'rxjs/operators'; +import { AuthState } from '../auth/auth.models'; +import { Observable, of } from 'rxjs'; +import { enterZone } from '@core/operator/enterZone'; +import { Authority } from '@shared/models/authority.enum'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthGuard implements CanActivate, CanActivateChild { + + constructor(private store: Store, + private authService: AuthService, + private dialogService: DialogService, + private translate: TranslateService, + private zone: NgZone) {} + + getAuthState(): Observable { + return this.store.pipe( + select(selectAuth), + skipWhile((authState) => !authState || !authState.isUserLoaded), + take(1), + enterZone(this.zone) + ); + } + + canActivate(next: ActivatedRouteSnapshot, + state: RouterStateSnapshot) { + + return this.getAuthState().pipe( + map((authState) => { + const url: string = state.url; + + let lastChild = state.root; + while (lastChild.children.length) { + lastChild = lastChild.children[0]; + } + const data = lastChild.data || {}; + const isPublic = data.module === 'public'; + + if (!authState.isAuthenticated) { + if (!isPublic) { + this.authService.redirectUrl = url; + // this.authService.gotoDefaultPlace(false); + return this.authService.defaultUrl(false); + } else { + return true; + } + } else { + if (url === '/login') { + // this.authService.gotoDefaultPlace(true); + return this.authService.defaultUrl(true); + } else { + const authority = Authority[authState.authUser.authority]; + if (data.auth && data.auth.indexOf(authority) === -1) { + this.dialogService.confirm( + this.translate.instant('access.access-forbidden'), + this.translate.instant('access.access-forbidden-text'), + this.translate.instant('action.cancel'), + this.translate.instant('action.sign-in'), + true + ).subscribe((res) => { + if (res) { + this.authService.logout(); + } + } + ); + return false; + } else { + return true; + } + } + } + }), + catchError((err => { console.error(err); return of(false); } )) + ); + } + + canActivateChild( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot) { + return this.canActivate(route, state); + } +} diff --git a/ui-ngx/src/app/core/guards/confirm-on-exit.guard.ts b/ui-ngx/src/app/core/guards/confirm-on-exit.guard.ts new file mode 100644 index 0000000000..ecf827d91c --- /dev/null +++ b/ui-ngx/src/app/core/guards/confirm-on-exit.guard.ts @@ -0,0 +1,68 @@ +/// +/// Copyright © 2016-2019 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 { Injectable } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanDeactivate, + RouterStateSnapshot +} from '@angular/router'; +import { FormGroup } from '@angular/forms'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AuthState } from '@core/auth/auth.models'; +import { selectAuth } from '@core/auth/auth.selectors'; +import { take } from 'rxjs/operators'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; + +export interface HasConfirmForm { + confirmForm(): FormGroup; +} + +@Injectable({ + providedIn: 'root' +}) +export class ConfirmOnExitGuard implements CanDeactivate { + + constructor(private store: Store, + private dialogService: DialogService, + private translate: TranslateService) { } + + canDeactivate(component: HasConfirmForm, + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot) { + + + let auth: AuthState = null; + this.store.pipe(select(selectAuth), take(1)).subscribe( + (authState: AuthState) => { + auth = authState; + } + ); + + if (component.confirmForm && auth && auth.isAuthenticated) { + const confirmForm = component.confirmForm(); + if (confirmForm && confirmForm.dirty) { + return this.dialogService.confirm( + this.translate.instant('confirm-on-exit.title'), + this.translate.instant('confirm-on-exit.html-message') + ); + } + } + return true; + } +} diff --git a/ui-ngx/src/app/core/http/http-utils.ts b/ui-ngx/src/app/core/http/http-utils.ts new file mode 100644 index 0000000000..7a5b38cf7c --- /dev/null +++ b/ui-ngx/src/app/core/http/http-utils.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2019 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 { InterceptorHttpParams } from '../interceptors/interceptor-http-params'; +import { HttpHeaders } from '@angular/common/http'; +import { InterceptorConfig } from '../interceptors/interceptor-config'; + +export function defaultHttpOptions(ignoreLoading: boolean = false, + ignoreErrors: boolean = false, + resendRequest: boolean = false) { + return { + headers: new HttpHeaders({'Content-Type': 'application/json'}), + params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest)) + }; +} diff --git a/ui-ngx/src/app/core/http/user.service.ts b/ui-ngx/src/app/core/http/user.service.ts new file mode 100644 index 0000000000..1e031289fd --- /dev/null +++ b/ui-ngx/src/app/core/http/user.service.ts @@ -0,0 +1,71 @@ +/// +/// Copyright © 2016-2019 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 { Injectable } from '@angular/core'; +import { defaultHttpOptions } from './http-utils'; +import { User } from '../../shared/models/user.model'; +import { Observable } from 'rxjs/index'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { AdminSettings } from '@shared/models/settings.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + + constructor( + private http: HttpClient + ) { } + + public getTenantAdmins(tenantId: string, pageLink: PageLink, + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable> { + return this.http.get>(`/api/tenant/${tenantId}/users${pageLink.toQuery()}`, + defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + + public getCustomerUsers(customerId: string, pageLink: PageLink, + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable> { + return this.http.get>(`/api/customer/${customerId}/users${pageLink.toQuery()}`, + defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + + public getUser(userId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable { + return this.http.get(`/api/user/${userId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + + public saveUser(user: User, sendActivationMail: boolean = false, + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable { + let url = '/api/user'; + url += '?sendActivationMail=' + sendActivationMail; + return this.http.post(url, user, defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + + public deleteUser(userId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { + return this.http.delete(`/api/user/${userId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + + public getActivationLink(userId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable { + return this.http.get(`/api/user/${userId}/activationLink`, + {...{responseType: 'text'}, ...defaultHttpOptions(ignoreLoading, ignoreErrors)}); + } + + public sendActivationEmail(email: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { + return this.http.post(`/api/user/sendActivationMail?email=${email}`, null, defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + +} diff --git a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts new file mode 100644 index 0000000000..3b87d3f6ed --- /dev/null +++ b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts @@ -0,0 +1,269 @@ +/// +/// Copyright © 2016-2019 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 { + HttpErrorResponse, + HttpEvent, + HttpHandler, + HttpInterceptor, + HttpRequest, + HttpResponseBase +} from '@angular/common/http'; +import { Observable } from 'rxjs/internal/Observable'; +import { Injectable } from '@angular/core'; +import { AuthService } from '../auth/auth.service'; +import { Constants } from '../../shared/models/constants'; +import { InterceptorHttpParams } from './interceptor-http-params'; +import {catchError, delay, switchMap, tap, map, mergeMap} from 'rxjs/operators'; +import { throwError } from 'rxjs/internal/observable/throwError'; +import { of } from 'rxjs/internal/observable/of'; +import { InterceptorConfig } from './interceptor-config'; +import { Store } from '@ngrx/store'; +import { AppState } from '../core.state'; +import { ActionLoadFinish, ActionLoadStart } from './load.actions'; +import { ActionNotificationShow } from '@app/core/notification/notification.actions'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; + +let tmpHeaders = {}; + +@Injectable() +export class GlobalHttpInterceptor implements HttpInterceptor { + + private AUTH_SCHEME = 'Bearer '; + private AUTH_HEADER_NAME = 'X-Authorization'; + + private internalUrlPrefixes = [ + '/api/auth/token' + ]; + + constructor(private store: Store, + private dialogService: DialogService, + private translate: TranslateService, + private authService: AuthService) { + } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + if (req.url.startsWith('/api/')) { + const config = this.getInterceptorConfig(req); + const isLoading = !this.isInternalUrlPrefix(req.url); + this.updateLoadingState(config, isLoading); + if (this.isTokenBasedAuthEntryPoint(req.url)) { + if (!AuthService.getJwtToken() && !this.authService.refreshTokenPending()) { + return this.handleResponseError(req, next, new HttpErrorResponse({error: {message: 'Unauthorized!'}, status: 401})); + } else if (!AuthService.isJwtTokenValid()) { + return this.handleResponseError(req, next, new HttpErrorResponse({error: {refreshTokenPending: true}})); + } else { + return this.jwtIntercept(req, next); + } + } else { + return this.handleRequest(req, next); + } + } else { + return next.handle(req); + } + } + + private jwtIntercept(req: HttpRequest, next: HttpHandler): Observable> { + const newReq = this.updateAuthorizationHeader(req); + if (newReq) { + return this.handleRequest(newReq, next); + } else { + return this.handleRequestError(req, new Error('Could not get JWT token from store.')); + } + } + + private handleRequest(req: HttpRequest, next: HttpHandler): Observable> { + return next.handle(req).pipe( + tap((event: HttpEvent) => { + if (event instanceof HttpResponseBase) { + this.handleResponse(req, event as HttpResponseBase); + } + }), + catchError((err) => { + const errorResponse = err as HttpErrorResponse; + return this.handleResponseError(req, next, errorResponse); + })); + } + + private handleRequestError(req: HttpRequest, err): Observable> { + const config = this.getInterceptorConfig(req); + if (req.url.startsWith('/api/')) { + this.updateLoadingState(config, false); + } + return throwError(err); + } + + private handleResponse(req: HttpRequest, response: HttpResponseBase) { + const config = this.getInterceptorConfig(req); + if (req.url.startsWith('/api/')) { + this.updateLoadingState(config, false); + } + } + + private handleResponseError(req: HttpRequest, next: HttpHandler, errorResponse: HttpErrorResponse): Observable> { + const config = this.getInterceptorConfig(req); + if (req.url.startsWith('/api/')) { + this.updateLoadingState(config, false); + } + let unhandled = false; + const ignoreErrors = config.ignoreErrors; + const resendRequest = config.resendRequest; + const errorCode = errorResponse.error ? errorResponse.error.errorCode : null; + if (errorResponse.error.refreshTokenPending || errorResponse.status === 401) { + if (errorResponse.error.refreshTokenPending || errorCode && errorCode === Constants.serverErrorCode.jwtTokenExpired) { + return this.refreshTokenAndRetry(req, next); + } else { + unhandled = true; + } + } else if (errorResponse.status === 429) { + if (resendRequest) { + return this.retryRequest(req, next); + } + } else if (errorResponse.status === 403) { + if (!ignoreErrors) { + this.permissionDenied(); + } + } else if (errorResponse.status === 0 || errorResponse.status === -1) { + this.showError('Unable to connect'); + } else if (!req.url.startsWith('/api/plugins/rpc')) { + if (errorResponse.status === 404) { + if (!ignoreErrors) { + this.showError(req.method + ': ' + req.url + '' + + errorResponse.status + ': ' + errorResponse.statusText); + } + } else { + unhandled = true; + } + } + + if (unhandled && !ignoreErrors) { + let error = null; + if (req.responseType === 'text') { + try { + error = errorResponse.error ? JSON.parse(errorResponse.error) : null; + } catch (e) {} + } else { + error = errorResponse.error; + } + if (error && !error.message) { + this.showError(this.prepareMessageFromData(error)); + } else if (error && error.message) { + this.showError(error.message, error.timeout ? error.timeout : 0); + } else { + this.showError('Unhandled error code ' + (error ? error.status : '\'Unknown\'')); + } + } + return throwError(errorResponse); + } + + private prepareMessageFromData(data) { + if (typeof data === 'object' && data.constructor === ArrayBuffer) { + const msg = String.fromCharCode.apply(null, new Uint8Array(data)); + try { + const msgObj = JSON.parse(msg); + if (msgObj.message) { + return msgObj.message; + } else { + return msg; + } + } catch (e) { + return msg; + } + } else { + return data; + } + } + + private retryRequest(req: HttpRequest, next: HttpHandler): Observable> { + const thisTimeout = 1000 + Math.random() * 3000; + return of(null).pipe( + delay(thisTimeout), + mergeMap(() => { + return this.jwtIntercept(req, next); + } + )); + } + + private refreshTokenAndRetry(req: HttpRequest, next: HttpHandler): Observable> { + return this.authService.refreshJwtToken().pipe(switchMap(() => { + return this.jwtIntercept(req, next); + }), + catchError((err: Error) => { + this.authService.logout(true); + const message = err ? err.message : 'Unauthorized!'; + return this.handleResponseError(req, next, new HttpErrorResponse({error: {message, timeout: 200}, status: 401})); + })); + } + + private updateAuthorizationHeader(req: HttpRequest): HttpRequest { + const jwtToken = AuthService.getJwtToken(); + if (jwtToken) { + req = req.clone({ + setHeaders: (tmpHeaders = {}, + tmpHeaders[this.AUTH_HEADER_NAME] = '' + this.AUTH_SCHEME + jwtToken, + tmpHeaders) + }); + return req; + } else { + return null; + } + } + + private isInternalUrlPrefix(url): boolean { + for (const index in this.internalUrlPrefixes) { + if (url.startsWith(this.internalUrlPrefixes[index])) { + return true; + } + } + return false; + } + + private isTokenBasedAuthEntryPoint(url): boolean { + return url.startsWith('/api/') && + !url.startsWith(Constants.entryPoints.login) && + !url.startsWith(Constants.entryPoints.tokenRefresh) && + !url.startsWith(Constants.entryPoints.nonTokenBased); + } + + private updateLoadingState(config: InterceptorConfig, isLoading: boolean) { + if (!config.ignoreLoading) { + this.store.dispatch(isLoading ? new ActionLoadStart() : new ActionLoadFinish()); + } + } + + private getInterceptorConfig(req: HttpRequest): InterceptorConfig { + if (req.params && req.params instanceof InterceptorHttpParams) { + return (req.params as InterceptorHttpParams).interceptorConfig; + } else { + return new InterceptorConfig(false, false); + } + } + + private permissionDenied() { + this.dialogService.alert( + this.translate.instant('access.permission-denied'), + this.translate.instant('access.permission-denied-text'), + this.translate.instant('action.close') + ); + } + + private showError(error: string, timeout: number = 0) { + setTimeout(() => { + this.store.dispatch(new ActionNotificationShow({message: error, type: 'error'})); + }, timeout); + } +} diff --git a/ui-ngx/src/app/core/interceptors/interceptor-config.ts b/ui-ngx/src/app/core/interceptors/interceptor-config.ts new file mode 100644 index 0000000000..ebe4d56453 --- /dev/null +++ b/ui-ngx/src/app/core/interceptors/interceptor-config.ts @@ -0,0 +1,21 @@ +/// +/// Copyright © 2016-2019 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. +/// + +export class InterceptorConfig { + constructor(public ignoreLoading: boolean = false, + public ignoreErrors: boolean = false, + public resendRequest: boolean = false) {} +} diff --git a/ui-ngx/src/app/core/interceptors/interceptor-http-params.ts b/ui-ngx/src/app/core/interceptors/interceptor-http-params.ts new file mode 100644 index 0000000000..d9e665a0fe --- /dev/null +++ b/ui-ngx/src/app/core/interceptors/interceptor-http-params.ts @@ -0,0 +1,27 @@ +/// +/// Copyright © 2016-2019 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 { HttpParams } from '@angular/common/http'; +import { InterceptorConfig } from './interceptor-config'; + +export class InterceptorHttpParams extends HttpParams { + constructor( + public interceptorConfig: InterceptorConfig, + params?: { [param: string]: string | string[] } + ) { + super({ fromObject: params }); + } +} diff --git a/ui-ngx/src/app/core/interceptors/load.actions.ts b/ui-ngx/src/app/core/interceptors/load.actions.ts new file mode 100644 index 0000000000..4f7387cb55 --- /dev/null +++ b/ui-ngx/src/app/core/interceptors/load.actions.ts @@ -0,0 +1,32 @@ +/// +/// Copyright © 2016-2019 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 { Action } from '@ngrx/store'; + +export enum LoadActionTypes { + START_LOAD = '[Load] Start', + FINISH_LOAD = '[Load] Finish' +} + +export class ActionLoadStart implements Action { + readonly type = LoadActionTypes.START_LOAD; +} + +export class ActionLoadFinish implements Action { + readonly type = LoadActionTypes.FINISH_LOAD; +} + +export type LoadActions = ActionLoadStart | ActionLoadFinish; diff --git a/ui-ngx/src/app/core/interceptors/load.models.ts b/ui-ngx/src/app/core/interceptors/load.models.ts new file mode 100644 index 0000000000..91fe7c9368 --- /dev/null +++ b/ui-ngx/src/app/core/interceptors/load.models.ts @@ -0,0 +1,19 @@ +/// +/// Copyright © 2016-2019 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. +/// + +export interface LoadState { + isLoading: boolean; +} diff --git a/ui-ngx/src/app/core/interceptors/load.reducer.ts b/ui-ngx/src/app/core/interceptors/load.reducer.ts new file mode 100644 index 0000000000..0a855fcb79 --- /dev/null +++ b/ui-ngx/src/app/core/interceptors/load.reducer.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2019 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 { LoadState } from './load.models'; +import { LoadActions, LoadActionTypes } from './load.actions'; + +export const initialState: LoadState = { + isLoading: false +}; + +export function loadReducer( + state: LoadState = initialState, + action: LoadActions +): LoadState { + switch (action.type) { + case LoadActionTypes.START_LOAD: + return { ...state, isLoading: true }; + + case LoadActionTypes.FINISH_LOAD: + return { ...state, isLoading: false }; + + default: + return state; + } +} diff --git a/ui-ngx/src/app/core/interceptors/load.selectors.ts b/ui-ngx/src/app/core/interceptors/load.selectors.ts new file mode 100644 index 0000000000..a34063c465 --- /dev/null +++ b/ui-ngx/src/app/core/interceptors/load.selectors.ts @@ -0,0 +1,34 @@ +/// +/// Copyright © 2016-2019 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 { createFeatureSelector, createSelector } from '@ngrx/store'; + +import { AppState } from '../core.state'; +import { LoadState } from './load.models'; + +export const selectLoadState = createFeatureSelector( + 'load' +); + +export const selectLoad = createSelector( + selectLoadState, + (state: LoadState) => state +); + +export const selectIsLoading = createSelector( + selectLoadState, + (state: LoadState) => state.isLoading +); diff --git a/ui-ngx/src/app/core/local-storage/local-storage.service.ts b/ui-ngx/src/app/core/local-storage/local-storage.service.ts new file mode 100644 index 0000000000..0c0bff21fc --- /dev/null +++ b/ui-ngx/src/app/core/local-storage/local-storage.service.ts @@ -0,0 +1,87 @@ +/// +/// Copyright © 2016-2019 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 { Injectable } from '@angular/core'; + +const APP_PREFIX = 'TB-'; + +@Injectable( + { + providedIn: 'root' + } +) +export class LocalStorageService { + constructor() {} + + static loadInitialState() { + return Object.keys(localStorage).reduce((state: any, storageKey) => { + if (storageKey.includes(APP_PREFIX)) { + const stateKeys = storageKey + .replace(APP_PREFIX, '') + .toLowerCase() + .split('.') + .map(key => + key + .split('-') + .map( + (token, index) => + index === 0 + ? token + : token.charAt(0).toUpperCase() + token.slice(1) + ) + .join('') + ); + let currentStateRef = state; + stateKeys.forEach((key, index) => { + if (index === stateKeys.length - 1) { + currentStateRef[key] = JSON.parse(localStorage.getItem(storageKey)); + return; + } + currentStateRef[key] = currentStateRef[key] || {}; + currentStateRef = currentStateRef[key]; + }); + } + return state; + }, {}); + } + + setItem(key: string, value: any) { + localStorage.setItem(`${APP_PREFIX}${key}`, JSON.stringify(value)); + } + + getItem(key: string) { + return JSON.parse(localStorage.getItem(`${APP_PREFIX}${key}`)); + } + + removeItem(key: string) { + localStorage.removeItem(`${APP_PREFIX}${key}`); + } + /** Tests that localStorage exists, can be written to, and read from. */ + testLocalStorage() { + const testValue = 'testValue'; + const testKey = 'testKey'; + let retrievedValue: string; + const errorMessage = 'localStorage did not return expected value'; + + this.setItem(testKey, testValue); + retrievedValue = this.getItem(testKey); + this.removeItem(testKey); + + if (retrievedValue !== testValue) { + throw new Error(errorMessage); + } + } +} diff --git a/ui-ngx/src/app/core/meta-reducers/debug.reducer.ts b/ui-ngx/src/app/core/meta-reducers/debug.reducer.ts new file mode 100644 index 0000000000..0d65341903 --- /dev/null +++ b/ui-ngx/src/app/core/meta-reducers/debug.reducer.ts @@ -0,0 +1,33 @@ +/// +/// Copyright © 2016-2019 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 { ActionReducer } from '@ngrx/store'; + +import { AppState } from '../core.state'; + +export function debug( + reducer: ActionReducer +): ActionReducer { + return (state, action) => { + const newState = reducer(state, action); + console.log(`[DEBUG] action: ${action.type}`, { + payload: (action as any).payload, + oldState: state, + newState + }); + return newState; + }; +} diff --git a/ui-ngx/src/app/core/meta-reducers/init-state-from-local-storage.reducer.ts b/ui-ngx/src/app/core/meta-reducers/init-state-from-local-storage.reducer.ts new file mode 100644 index 0000000000..3091491a99 --- /dev/null +++ b/ui-ngx/src/app/core/meta-reducers/init-state-from-local-storage.reducer.ts @@ -0,0 +1,32 @@ +/// +/// Copyright © 2016-2019 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 { ActionReducer, INIT, UPDATE } from '@ngrx/store'; + +import { LocalStorageService } from '../local-storage/local-storage.service'; +import { AppState } from '../core.state'; + +export function initStateFromLocalStorage( + reducer: ActionReducer +): ActionReducer { + return (state, action) => { + const newState = reducer(state, action); + if ([INIT.toString(), UPDATE.toString()].includes(action.type)) { + return { ...newState, ...LocalStorageService.loadInitialState() }; + } + return newState; + }; +} diff --git a/ui-ngx/src/app/core/notification/notification.actions.ts b/ui-ngx/src/app/core/notification/notification.actions.ts new file mode 100644 index 0000000000..aa1fb865af --- /dev/null +++ b/ui-ngx/src/app/core/notification/notification.actions.ts @@ -0,0 +1,31 @@ +/// +/// Copyright © 2016-2019 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 { Action } from '@ngrx/store'; +import { NotificationMessage } from '@app/core/notification/notification.models'; + +export enum NotificationActionTypes { + SHOW_NOTIFICATION = '[Notification] Show' +} + +export class ActionNotificationShow implements Action { + readonly type = NotificationActionTypes.SHOW_NOTIFICATION; + + constructor(readonly notification: NotificationMessage ) {} +} + +export type NotificationActions = + | ActionNotificationShow; diff --git a/ui-ngx/src/app/core/notification/notification.effects.ts b/ui-ngx/src/app/core/notification/notification.effects.ts new file mode 100644 index 0000000000..dd687cc8be --- /dev/null +++ b/ui-ngx/src/app/core/notification/notification.effects.ts @@ -0,0 +1,47 @@ +/// +/// Copyright © 2016-2019 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 { Injectable } from '@angular/core'; +import { Actions, Effect, ofType } from '@ngrx/effects'; +import { + map, +} from 'rxjs/operators'; + +import { + NotificationActions, + NotificationActionTypes +} from '@app/core/notification/notification.actions'; +import { NotificationService } from '@app/core/services/notification.service'; + +@Injectable() +export class NotificationEffects { + constructor( + private actions$: Actions, + private notificationService: NotificationService + ) { + } + + @Effect({dispatch: false}) + dispatchNotification = this.actions$.pipe( + ofType( + NotificationActionTypes.SHOW_NOTIFICATION, + ), + map(({ notification }) => { + this.notificationService.dispatchNotification(notification); + }) + ); + +} diff --git a/ui-ngx/src/app/core/notification/notification.models.ts b/ui-ngx/src/app/core/notification/notification.models.ts new file mode 100644 index 0000000000..f0fc4135c3 --- /dev/null +++ b/ui-ngx/src/app/core/notification/notification.models.ts @@ -0,0 +1,33 @@ +/// +/// Copyright © 2016-2019 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. +/// + + +export interface NotificationState { + notification: NotificationMessage; +} + +export declare type NotificationType = 'info' | 'success' | 'error'; +export declare type NotificationHorizontalPosition = 'start' | 'center' | 'end' | 'left' | 'right'; +export declare type NotificationVerticalPosition = 'top' | 'bottom'; + +export class NotificationMessage { + message: string; + type: NotificationType; + target?: string; + duration?: number; + horizontalPosition?: NotificationHorizontalPosition; + verticalPosition?: NotificationVerticalPosition; +} diff --git a/ui-ngx/src/app/core/notification/notification.reducer.ts b/ui-ngx/src/app/core/notification/notification.reducer.ts new file mode 100644 index 0000000000..81c0387fa2 --- /dev/null +++ b/ui-ngx/src/app/core/notification/notification.reducer.ts @@ -0,0 +1,34 @@ +/// +/// Copyright © 2016-2019 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 { NotificationState } from './notification.models'; +import { NotificationActions, NotificationActionTypes } from './notification.actions'; + +export const initialState: NotificationState = { + notification: null +}; + +export function notificationReducer( + state: NotificationState = initialState, + action: NotificationActions +): NotificationState { + switch (action.type) { + case NotificationActionTypes.SHOW_NOTIFICATION: + return { ...state, notification: action.notification }; + default: + return state; + } +} diff --git a/ui-ngx/src/app/core/notification/notification.selectors.ts b/ui-ngx/src/app/core/notification/notification.selectors.ts new file mode 100644 index 0000000000..b075ae3b39 --- /dev/null +++ b/ui-ngx/src/app/core/notification/notification.selectors.ts @@ -0,0 +1,29 @@ +/// +/// Copyright © 2016-2019 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 { createFeatureSelector, createSelector } from '@ngrx/store'; + +import { NotificationState } from './notification.models'; +import { AppState } from '@app/core/core.state'; + +export const selectNotificationState = createFeatureSelector( + 'notification' +); + +export const selectNotification = createSelector( + selectNotificationState, + (state: NotificationState) => state +); diff --git a/ui-ngx/src/app/core/operator/enterZone.ts b/ui-ngx/src/app/core/operator/enterZone.ts new file mode 100644 index 0000000000..e55367f41c --- /dev/null +++ b/ui-ngx/src/app/core/operator/enterZone.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2019 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 { MonoTypeOperatorFunction, Observable, Operator, Subscriber } from 'rxjs'; + +export type EnterZoneSignature = (zone: { run: (fn: any) => any }) => Observable; + +export function enterZone(zone: { run: (fn: any) => any }): MonoTypeOperatorFunction { + return (source: Observable) => { + return source.lift(new EnterZoneOperator(zone)); + }; +} + +export class EnterZoneOperator implements Operator { + constructor(private zone: { run: (fn: any) => any }) { } + + call(subscriber: Subscriber, source: any): any { + return source._subscribe(new EnterZoneSubscriber(subscriber, this.zone)); + } +} + +class EnterZoneSubscriber extends Subscriber { + constructor(destination: Subscriber, private zone: { run: (fn: any) => any }) { + super(destination); + } + + protected _next(value: T) { + this.zone.run(() => this.destination.next(value)); + } +} diff --git a/ui-ngx/src/app/core/services/dialog.service.ts b/ui-ngx/src/app/core/services/dialog.service.ts new file mode 100644 index 0000000000..955e062dce --- /dev/null +++ b/ui-ngx/src/app/core/services/dialog.service.ts @@ -0,0 +1,70 @@ +/// +/// Copyright © 2016-2019 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 { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { MatDialog, MatDialogConfig } from '@angular/material'; +import { ConfirmDialogComponent } from '@core/services/dialog/confirm-dialog.component'; +import { TranslateService } from '@ngx-translate/core'; +import { AlertDialogComponent } from '@core/services/dialog/alert-dialog.component'; + +@Injectable( + { + providedIn: 'root' + } +) +export class DialogService { + + constructor( + private translate: TranslateService, + public dialog: MatDialog + ) { + } + + confirm(title: string, message: string, cancel: string = null, ok: string = null, fullscreen: boolean = false): Observable { + const dialogConfig: MatDialogConfig = { + disableClose: true, + data: { + title, + message, + cancel: cancel || this.translate.instant('action.cancel'), + ok: ok || this.translate.instant('action.ok') + } + }; + if (fullscreen) { + dialogConfig.panelClass = ['tb-fullscreen-dialog']; + } + const dialogRef = this.dialog.open(ConfirmDialogComponent, dialogConfig); + return dialogRef.afterClosed(); + } + + alert(title: string, message: string, ok: string = null, fullscreen: boolean = false): Observable { + const dialogConfig: MatDialogConfig = { + disableClose: true, + data: { + title, + message, + ok: ok || this.translate.instant('action.ok') + } + }; + if (fullscreen) { + dialogConfig.panelClass = ['tb-fullscreen-dialog']; + } + const dialogRef = this.dialog.open(AlertDialogComponent, dialogConfig); + return dialogRef.afterClosed(); + } + +} diff --git a/ui-ngx/src/app/core/services/dialog/alert-dialog.component.html b/ui-ngx/src/app/core/services/dialog/alert-dialog.component.html new file mode 100644 index 0000000000..aa959330c0 --- /dev/null +++ b/ui-ngx/src/app/core/services/dialog/alert-dialog.component.html @@ -0,0 +1,23 @@ + +{{data.title}} + + + + {{data.ok}} + diff --git a/ui-ngx/src/app/core/services/dialog/alert-dialog.component.scss b/ui-ngx/src/app/core/services/dialog/alert-dialog.component.scss new file mode 100644 index 0000000000..c15dc6cd28 --- /dev/null +++ b/ui-ngx/src/app/core/services/dialog/alert-dialog.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2019 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. + */ +:host { + .mat-dialog-content { + padding: 0 24px 24px; + } +} diff --git a/ui-ngx/src/app/core/services/dialog/alert-dialog.component.ts b/ui-ngx/src/app/core/services/dialog/alert-dialog.component.ts new file mode 100644 index 0000000000..b005609bfd --- /dev/null +++ b/ui-ngx/src/app/core/services/dialog/alert-dialog.component.ts @@ -0,0 +1,34 @@ +/// +/// Copyright © 2016-2019 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 { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; + +export interface AlertDialogData { + title: string; + message: string; + ok: string; +} + +@Component({ + selector: 'tb-alert-dialog', + templateUrl: './alert-dialog.component.html', + styleUrls: ['./alert-dialog.component.scss'] +}) +export class AlertDialogComponent { + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: AlertDialogData) {} +} diff --git a/ui-ngx/src/app/core/services/dialog/confirm-dialog.component.html b/ui-ngx/src/app/core/services/dialog/confirm-dialog.component.html new file mode 100644 index 0000000000..7e7fd5a5d2 --- /dev/null +++ b/ui-ngx/src/app/core/services/dialog/confirm-dialog.component.html @@ -0,0 +1,24 @@ + +{{data.title}} + + + + {{data.cancel}} + {{data.ok}} + diff --git a/ui-ngx/src/app/core/services/dialog/confirm-dialog.component.scss b/ui-ngx/src/app/core/services/dialog/confirm-dialog.component.scss new file mode 100644 index 0000000000..c15dc6cd28 --- /dev/null +++ b/ui-ngx/src/app/core/services/dialog/confirm-dialog.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2019 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. + */ +:host { + .mat-dialog-content { + padding: 0 24px 24px; + } +} diff --git a/ui-ngx/src/app/core/services/dialog/confirm-dialog.component.ts b/ui-ngx/src/app/core/services/dialog/confirm-dialog.component.ts new file mode 100644 index 0000000000..13c384624a --- /dev/null +++ b/ui-ngx/src/app/core/services/dialog/confirm-dialog.component.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2019 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 { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; + +export interface ConfirmDialogData { + title: string; + message: string; + cancel: string; + ok: string; +} + +@Component({ + selector: 'tb-confirm-dialog', + templateUrl: './confirm-dialog.component.html', + styleUrls: ['./confirm-dialog.component.scss'] +}) +export class ConfirmDialogComponent { + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ConfirmDialogData) {} +} diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts new file mode 100644 index 0000000000..eb822d7833 --- /dev/null +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -0,0 +1,39 @@ +/// +/// Copyright © 2016-2019 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. +/// + +export declare type MenuSectionType = 'link' | 'toggle'; + +export class MenuSection { + name: string; + type: MenuSectionType; + path: string; + icon: string; + isMdiIcon?: boolean; + height?: string; + pages?: Array; +} + +export class HomeSection { + name: string; + places: Array; +} + +export class HomeSectionPlace { + name: string; + icon: string; + isMdiIcon?: boolean; + path: string; +} diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts new file mode 100644 index 0000000000..463513052e --- /dev/null +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -0,0 +1,344 @@ +/// +/// Copyright © 2016-2019 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 { Injectable } from '@angular/core'; +import { AuthService } from '../auth/auth.service'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '../core.state'; +import { selectAuthUser, selectIsAuthenticated } from '../auth/auth.selectors'; +import { take } from 'rxjs/operators'; +import { HomeSection, MenuSection } from '@core/services/menu.models'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { Authority } from '@shared/models/authority.enum'; +import {AuthUser} from '@shared/models/user.model'; + +@Injectable({ + providedIn: 'root' +}) +export class MenuService { + + menuSections$: Subject> = new BehaviorSubject>([]); + homeSections$: Subject> = new BehaviorSubject>([]); + + constructor(private store: Store, private authService: AuthService) { + this.store.pipe(select(selectIsAuthenticated)).subscribe( + (authenticated: boolean) => { + if (authenticated) { + this.buildMenu(); + } + } + ); + } + + private buildMenu() { + this.store.pipe(select(selectAuthUser), take(1)).subscribe( + (authUser: AuthUser) => { + if (authUser) { + let menuSections: Array; + let homeSections: Array; + switch (authUser.authority) { + case Authority.SYS_ADMIN: + menuSections = this.buildSysAdminMenu(authUser); + homeSections = this.buildSysAdminHome(authUser); + break; + case Authority.TENANT_ADMIN: + menuSections = this.buildTenantAdminMenu(authUser); + homeSections = this.buildTenantAdminHome(authUser); + break; + case Authority.CUSTOMER_USER: + menuSections = this.buildCustomerUserMenu(authUser); + homeSections = this.buildCustomerUserHome(authUser); + break; + } + this.menuSections$.next(menuSections); + this.homeSections$.next(homeSections); + } + } + ); + } + + private buildSysAdminMenu(authUser: any): Array { + const sections: Array = []; + sections.push( + { + name: 'home.home', + type: 'link', + path: '/home', + icon: 'home' + }, + { + name: 'tenant.tenants', + type: 'link', + path: '/tenants', + icon: 'supervisor_account' + }, + { + name: 'widget.widget-library', + type: 'link', + path: '/widgets-bundles', + icon: 'now_widgets' + }, + { + name: 'admin.system-settings', + type: 'toggle', + path: '/settings', + height: '120px', + icon: 'settings', + pages: [ + { + name: 'admin.general', + type: 'link', + path: '/settings/general', + icon: 'settings_applications' + }, + { + name: 'admin.outgoing-mail', + type: 'link', + path: '/settings/outgoing-mail', + icon: 'mail' + }, + { + name: 'admin.security-settings', + type: 'link', + path: '/settings/security-settings', + icon: 'security' + } + ] + } + ); + return sections; + } + + private buildSysAdminHome(authUser: any): Array { + const homeSections: Array = []; + homeSections.push( + { + name: 'tenant.management', + places: [ + { + name: 'tenant.tenants', + icon: 'supervisor_account', + path: '/tenants' + } + ] + }, + { + name: 'widget.management', + places: [ + { + name: 'widget.widget-library', + icon: 'now_widgets', + path: '/widgets-bundles' + } + ] + }, + { + name: 'admin.system-settings', + places: [ + { + name: 'admin.general', + icon: 'settings_applications', + path: '/settings/general' + }, + { + name: 'admin.outgoing-mail', + icon: 'mail', + path: '/settings/outgoing-mail' + }, + { + name: 'admin.security-settings', + icon: 'security', + path: '/settings/security-settings' + } + ] + } + ); + return homeSections; + } + + private buildTenantAdminMenu(authUser: any): Array { + const sections: Array = []; + sections.push( + { + name: 'home.home', + type: 'link', + path: '/home', + icon: 'home' + }, + { + name: 'rulechain.rulechains', + type: 'link', + path: '/ruleChains', + icon: 'settings_ethernet' + }, + { + name: 'customer.customers', + type: 'link', + path: '/customers', + icon: 'supervisor_account' + }, + { + name: 'asset.assets', + type: 'link', + path: '/assets', + icon: 'domain' + }, + { + name: 'device.devices', + type: 'link', + path: '/devices', + icon: 'devices_other' + }, + { + name: 'entity-view.entity-views', + type: 'link', + path: '/entityViews', + icon: 'view_quilt' + }, + { + name: 'widget.widget-library', + type: 'link', + path: '/widgets-bundles', + icon: 'now_widgets' + }, + { + name: 'dashboard.dashboards', + type: 'link', + path: '/dashboards', + icon: 'dashboards' + }, + { + name: 'audit-log.audit-logs', + type: 'link', + path: '/auditLogs', + icon: 'track_changes' + } + ); + return sections; + } + + private buildTenantAdminHome(authUser: any): Array { + const homeSections: Array = []; + homeSections.push( + { + name: 'rulechain.management', + places: [ + { + name: 'rulechain.rulechains', + icon: 'settings_ethernet', + path: '/ruleChains' + } + ] + }, + { + name: 'customer.management', + places: [ + { + name: 'customer.customers', + icon: 'supervisor_account', + path: '/customers' + } + ] + }, + { + name: 'asset.management', + places: [ + { + name: 'asset.assets', + icon: 'domain', + path: '/assets' + } + ] + }, + { + name: 'device.management', + places: [ + { + name: 'device.devices', + icon: 'devices_other', + path: '/devices' + } + ] + }, + { + name: 'entity-view.management', + places: [ + { + name: 'entity-view.entity-views', + icon: 'view_quilt', + path: '/entityViews' + } + ] + }, + { + name: 'dashboard.management', + places: [ + { + name: 'widget.widget-library', + icon: 'now_widgets', + path: '/widgets-bundles' + }, + { + name: 'dashboard.dashboards', + icon: 'dashboard', + path: '/dashboards' + } + ] + }, + { + name: 'audit-log.audit', + places: [ + { + name: 'audit-log.audit-logs', + icon: 'track_changes', + path: '/auditLogs' + } + ] + } + ); + return homeSections; + } + + private buildCustomerUserMenu(authUser: any): Array { + const sections: Array = []; + sections.push( + { + name: 'home.home', + type: 'link', + path: '/home', + icon: 'home' + } + ); + // TODO: + return sections; + } + + private buildCustomerUserHome(authUser: any): Array { + const homeSections: Array = []; + // TODO: + return homeSections; + } + + public menuSections(): Observable> { + return this.menuSections$; + } + + public homeSections(): Observable> { + return this.homeSections$; + } + +} + diff --git a/ui-ngx/src/app/core/services/notification.service.ts b/ui-ngx/src/app/core/services/notification.service.ts new file mode 100644 index 0000000000..fed93cf93a --- /dev/null +++ b/ui-ngx/src/app/core/services/notification.service.ts @@ -0,0 +1,43 @@ +/// +/// Copyright © 2016-2019 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 { Injectable } from '@angular/core'; +import { NotificationMessage } from '@app/core/notification/notification.models'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; + + +@Injectable( + { + providedIn: 'root' + } +) +export class NotificationService { + + private notificationSubject: Subject = new Subject(); + + constructor( + ) { + } + + dispatchNotification(notification: NotificationMessage) { + this.notificationSubject.next(notification); + } + + getNotification(): Observable { + return this.notificationSubject; + } + +} diff --git a/ui-ngx/src/app/core/services/time.service.ts b/ui-ngx/src/app/core/services/time.service.ts new file mode 100644 index 0000000000..070356abbc --- /dev/null +++ b/ui-ngx/src/app/core/services/time.service.ts @@ -0,0 +1,123 @@ +/// +/// Copyright © 2016-2019 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 { Injectable } from '@angular/core'; +import { DAY, defaultTimeIntervals, SECOND } from '@shared/models/time/time.models'; +import {HttpClient} from '@angular/common/http'; +import {Observable} from 'rxjs'; +import {defaultHttpOptions} from '@core/http/http-utils'; +import {map} from 'rxjs/operators'; + +export interface TimeInterval { + name: string; + translateParams: {[key: string]: any}; + value: number; +} + +const MIN_INTERVAL = SECOND; +const MAX_INTERVAL = 365 * 20 * DAY; + +const MIN_LIMIT = 10; + +const MAX_DATAPOINTS_LIMIT = 500; + +@Injectable({ + providedIn: 'root' +}) +export class TimeService { + + private maxDatapointsLimit = MAX_DATAPOINTS_LIMIT; + + constructor( + private http: HttpClient + ) {} + + public loadMaxDatapointsLimit(): Observable { + return this.http.get('/api/dashboard/maxDatapointsLimit', + defaultHttpOptions(true)).pipe( + map( (limit) => { + this.maxDatapointsLimit = limit; + if (!this.maxDatapointsLimit || this.maxDatapointsLimit <= MIN_LIMIT) { + this.maxDatapointsLimit = MIN_LIMIT + 1; + } + return this.maxDatapointsLimit; + }) + ); + } + + public matchesExistingInterval(min: number, max: number, intervalMs: number): boolean { + const intervals = this.getIntervals(min, max); + return intervals.findIndex(interval => interval.value === intervalMs) > -1; + } + + public getIntervals(min: number, max: number): Array { + min = this.boundMinInterval(min); + max = this.boundMaxInterval(max); + return defaultTimeIntervals.filter((interval) => interval.value >= min && interval.value <= max); + } + + public boundMinInterval(min: number): number { + return this.toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL); + } + + public boundMaxInterval(max: number): number { + return this.toBound(max, MIN_INTERVAL, MAX_INTERVAL, MAX_INTERVAL); + } + + public boundToPredefinedInterval(min: number, max: number, intervalMs: number): number { + const intervals = this.getIntervals(min, max); + let minDelta = MAX_INTERVAL; + const boundedInterval = intervalMs || min; + let matchedInterval: TimeInterval = intervals[0]; + intervals.forEach((interval) => { + const delta = Math.abs(interval.value - boundedInterval); + if (delta < minDelta) { + matchedInterval = interval; + minDelta = delta; + } + }); + return matchedInterval.value; + } + + public getMaxDatapointsLimit(): number { + return this.maxDatapointsLimit; + } + + public getMinDatapointsLimit(): number { + return MIN_LIMIT; + } + + public minIntervalLimit(timewindowMs: number): number { + const min = timewindowMs / 500; + return this.boundMinInterval(min); + } + + public maxIntervalLimit(timewindowMs: number): number { + const max = timewindowMs / MIN_LIMIT; + return this.boundMaxInterval(max); + } + + private toBound(value: number, min: number, max: number, defValue: number): number { + if (typeof value !== 'undefined') { + value = Math.max(value, min); + value = Math.min(value, max); + return value; + } else { + return defValue; + } + } + +} diff --git a/ui-ngx/src/app/core/services/title.service.ts b/ui-ngx/src/app/core/services/title.service.ts new file mode 100644 index 0000000000..9e7a501e0f --- /dev/null +++ b/ui-ngx/src/app/core/services/title.service.ts @@ -0,0 +1,55 @@ +/// +/// Copyright © 2016-2019 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 { Title } from '@angular/platform-browser'; +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { filter } from 'rxjs/operators'; + +import { environment as env } from '@env/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class TitleService { + constructor( + private translate: TranslateService, + private title: Title + ) {} + + setTitle( + snapshot: ActivatedRouteSnapshot, + lazyTranslate?: TranslateService + ) { + let lastChild = snapshot; + while (lastChild.children.length) { + lastChild = lastChild.children[0]; + } + const { title } = lastChild.data; + const translate = lazyTranslate || this.translate; + if (title) { + translate + .get(title) + .pipe(filter(translatedTitle => translatedTitle !== title)) + .subscribe(translatedTitle => + this.title.setTitle(`${env.appTitle} | ${translatedTitle}`) + ); + } else { + this.title.setTitle(env.appTitle); + } + } +} diff --git a/ui-ngx/src/app/core/services/window.service.ts b/ui-ngx/src/app/core/services/window.service.ts new file mode 100644 index 0000000000..2309b4ec94 --- /dev/null +++ b/ui-ngx/src/app/core/services/window.service.ts @@ -0,0 +1,71 @@ +/// +/// Copyright © 2016-2019 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 { isPlatformBrowser } from '@angular/common'; +import { ClassProvider, FactoryProvider, InjectionToken, PLATFORM_ID } from '@angular/core'; + +/* Create a new injection token for injecting the window into a component. */ +export const WINDOW = new InjectionToken('WindowToken'); + +/* Define abstract class for obtaining reference to the global window object. */ +export abstract class WindowRef { + + get nativeWindow(): Window | object { + throw new Error('Not implemented.'); + } + +} + +/* Define class that implements the abstract class and returns the native window object. */ +export class BrowserWindowRef extends WindowRef { + + constructor() { + super(); + } + + get nativeWindow(): Window | object { + return window; + } + +} + +/* Create an factory function that returns the native window object. */ +export function windowFactory(browserWindowRef: BrowserWindowRef, platformId: object): Window | object { + if (isPlatformBrowser(platformId)) { + return browserWindowRef.nativeWindow; + } + return new Object(); +} + +/* Create a injectable provider for the WindowRef token that uses the BrowserWindowRef class. */ +export const browserWindowProvider: ClassProvider = { + provide: WindowRef, + useClass: BrowserWindowRef +}; + +/* Create an injectable provider that uses the windowFactory function for returning the native window object. */ +export const windowProvider: FactoryProvider = { + provide: WINDOW, + useFactory: windowFactory, + deps: [ WindowRef, PLATFORM_ID ] +}; + +/* Create an array of providers. */ +export const WINDOW_PROVIDERS = [ + browserWindowProvider, + windowProvider +]; diff --git a/ui-ngx/src/app/core/settings/settings.actions.ts b/ui-ngx/src/app/core/settings/settings.actions.ts new file mode 100644 index 0000000000..3358dc572d --- /dev/null +++ b/ui-ngx/src/app/core/settings/settings.actions.ts @@ -0,0 +1,30 @@ +/// +/// Copyright © 2016-2019 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 { Action } from '@ngrx/store'; + +export enum SettingsActionTypes { + CHANGE_LANGUAGE = '[Settings] Change Language' +} + +export class ActionSettingsChangeLanguage implements Action { + readonly type = SettingsActionTypes.CHANGE_LANGUAGE; + + constructor(readonly payload: { userLang: string }) {} +} + +export type SettingsActions = + | ActionSettingsChangeLanguage; diff --git a/ui-ngx/src/app/core/settings/settings.effects.ts b/ui-ngx/src/app/core/settings/settings.effects.ts new file mode 100644 index 0000000000..e0b4993719 --- /dev/null +++ b/ui-ngx/src/app/core/settings/settings.effects.ts @@ -0,0 +1,88 @@ +/// +/// Copyright © 2016-2019 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 { ActivationEnd, Router } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { select, Store } from '@ngrx/store'; +import { Actions, Effect, ofType } from '@ngrx/effects'; +import { TranslateService } from '@ngx-translate/core'; +import { merge } from 'rxjs'; +import { + tap, + withLatestFrom, + map, + distinctUntilChanged, + filter +} from 'rxjs/operators'; + +import { + SettingsActionTypes, + SettingsActions, +} from './settings.actions'; +import { + selectSettingsState +} from './settings.selectors'; +import { AppState } from '@app/core/core.state'; +import { LocalStorageService } from '@app/core/local-storage/local-storage.service'; +import { TitleService } from '@app/core/services/title.service'; +import { updateUserLang } from '@app/core/settings/settings.utils'; + +export const SETTINGS_KEY = 'SETTINGS'; + +@Injectable() +export class SettingsEffects { + constructor( + private actions$: Actions, + private store: Store, + private router: Router, + private localStorageService: LocalStorageService, + private titleService: TitleService, + private translate: TranslateService + ) { + } + + @Effect({dispatch: false}) + persistSettings = this.actions$.pipe( + ofType( + SettingsActionTypes.CHANGE_LANGUAGE, + ), + withLatestFrom(this.store.pipe(select(selectSettingsState))), + tap(([action, settings]) => + this.localStorageService.setItem(SETTINGS_KEY, settings) + ) + ); + + @Effect({dispatch: false}) + setTranslateServiceLanguage = this.store.pipe( + select(selectSettingsState), + map(settings => settings.userLang), + distinctUntilChanged(), + tap(userLang => updateUserLang(this.translate, userLang)) + ); + + @Effect({dispatch: false}) + setTitle = merge( + this.actions$.pipe(ofType(SettingsActionTypes.CHANGE_LANGUAGE)), + this.router.events.pipe(filter(event => event instanceof ActivationEnd)) + ).pipe( + tap(() => { + this.titleService.setTitle( + this.router.routerState.snapshot.root, + this.translate + ); + }) + ); +} diff --git a/ui-ngx/src/app/core/settings/settings.models.ts b/ui-ngx/src/app/core/settings/settings.models.ts new file mode 100644 index 0000000000..f7fc433711 --- /dev/null +++ b/ui-ngx/src/app/core/settings/settings.models.ts @@ -0,0 +1,20 @@ +/// +/// Copyright © 2016-2019 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. +/// + + +export interface SettingsState { + userLang: string; +} diff --git a/ui-ngx/src/app/core/settings/settings.reducer.ts b/ui-ngx/src/app/core/settings/settings.reducer.ts new file mode 100644 index 0000000000..aa6f4ae982 --- /dev/null +++ b/ui-ngx/src/app/core/settings/settings.reducer.ts @@ -0,0 +1,34 @@ +/// +/// Copyright © 2016-2019 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 { SettingsState } from './settings.models'; +import { SettingsActions, SettingsActionTypes } from './settings.actions'; + +export const initialState: SettingsState = { + userLang: null +}; + +export function settingsReducer( + state: SettingsState = initialState, + action: SettingsActions +): SettingsState { + switch (action.type) { + case SettingsActionTypes.CHANGE_LANGUAGE: + return { ...state, ...action.payload }; + default: + return state; + } +} diff --git a/ui-ngx/src/app/core/settings/settings.selectors.ts b/ui-ngx/src/app/core/settings/settings.selectors.ts new file mode 100644 index 0000000000..09ff660433 --- /dev/null +++ b/ui-ngx/src/app/core/settings/settings.selectors.ts @@ -0,0 +1,34 @@ +/// +/// Copyright © 2016-2019 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 { createFeatureSelector, createSelector } from '@ngrx/store'; + +import { SettingsState } from './settings.models'; +import { AppState } from '@app/core/core.state'; + +export const selectSettingsState = createFeatureSelector( + 'settings' +); + +export const selectSettings = createSelector( + selectSettingsState, + (state: SettingsState) => state +); + +export const selectUserLang = createSelector( + selectSettings, + (state: SettingsState) => state.userLang +); diff --git a/ui-ngx/src/app/core/settings/settings.utils.ts b/ui-ngx/src/app/core/settings/settings.utils.ts new file mode 100644 index 0000000000..1f7a24d7d5 --- /dev/null +++ b/ui-ngx/src/app/core/settings/settings.utils.ts @@ -0,0 +1,57 @@ +/// +/// Copyright © 2016-2019 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 { environment } from '@env/environment'; +import { TranslateService } from '@ngx-translate/core'; + +export function updateUserLang(translate: TranslateService, userLang: string) { + let targetLang = userLang; + console.log(`User lang: ${targetLang}`); + if (!targetLang) { + targetLang = translate.getBrowserCultureLang(); + console.log(`Fallback to browser lang: ${targetLang}`); + } + const detectedSupportedLang = detectSupportedLang(targetLang); + console.log(`Detected supported lang: ${detectedSupportedLang}`); + translate.use(detectedSupportedLang); +} + +function detectSupportedLang(targetLang: string): string { + const langTag = (targetLang || '').split('-').join('_'); + if (langTag.length) { + if (environment.supportedLangs.indexOf(langTag) > -1) { + return langTag; + } else { + const parts = langTag.split('_'); + let lang; + if (parts.length === 2) { + lang = parts[0]; + } else { + lang = langTag; + } + const foundLangs = environment.supportedLangs.filter( + (supportedLang: string) => { + const supportedLangParts = supportedLang.split('_'); + return supportedLangParts[0] === lang; + } + ); + if (foundLangs.length) { + return foundLangs[0]; + } + } + } + return environment.defaultLang; +} diff --git a/ui-ngx/src/app/core/translate/missing-translate-handler.ts b/ui-ngx/src/app/core/translate/missing-translate-handler.ts new file mode 100644 index 0000000000..71cf578b2b --- /dev/null +++ b/ui-ngx/src/app/core/translate/missing-translate-handler.ts @@ -0,0 +1,23 @@ +/// +/// Copyright © 2016-2019 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 {MissingTranslationHandler, MissingTranslationHandlerParams} from '@ngx-translate/core'; + +export class TbMissingTranslationHandler implements MissingTranslationHandler { + handle(params: MissingTranslationHandlerParams) { + console.warn('Translation for ' + params.key + ' doesn\'t exist'); + } +} diff --git a/ui-ngx/src/app/core/translate/translate-default-compiler.ts b/ui-ngx/src/app/core/translate/translate-default-compiler.ts new file mode 100644 index 0000000000..3e92f3b64c --- /dev/null +++ b/ui-ngx/src/app/core/translate/translate-default-compiler.ts @@ -0,0 +1,67 @@ +/// +/// Copyright © 2016-2019 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 { + MESSAGE_FORMAT_CONFIG, MessageFormatConfig, + TranslateMessageFormatCompiler +} from 'ngx-translate-messageformat-compiler'; +import { Inject, Optional } from '@angular/core'; +const parse = require('messageformat-parser').parse; + +export class TranslateDefaultCompiler extends TranslateMessageFormatCompiler { + + constructor( + @Optional() + @Inject(MESSAGE_FORMAT_CONFIG) + config?: MessageFormatConfig + ) { + super(config); + } + + public compile(value: string, lang: string): (params: any) => string { + return this.defaultCompile(value, lang); + } + + public compileTranslations(translations: any, lang: string): any { + return this.defaultCompile(translations, lang); + } + + private defaultCompile(src: any, lang: string): any { + if (typeof src !== 'object') { + if (this.checkIsPlural(src)) { + return super.compile(src, lang); + } else { + return src; + } + } else { + const result = {}; + for (const key of Object.keys(src)) { + result[key] = this.defaultCompile(src[key], lang); + } + return result; + } + } + + private checkIsPlural(src: string): boolean { + const tokens: any[] = parse(src.replace(/\{\{/g, '{').replace(/\}\}/g, '}'), + {cardinal: [], ordinal: []}); + const res = tokens.filter( + (value) => typeof value !== 'string' && value.type === 'plural' + ); + return res.length > 0; + } + +} diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts new file mode 100644 index 0000000000..b25fa860e9 --- /dev/null +++ b/ui-ngx/src/app/core/utils.ts @@ -0,0 +1,86 @@ +/// +/// Copyright © 2016-2019 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 { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { finalize, share } from 'rxjs/operators'; + +export function onParentScrollOrWindowResize(el: Node): Observable { + const scrollSubject = new Subject(); + const scrollParentNodes = scrollParents(el); + const eventListenerObject: EventListenerObject = { + handleEvent(evt: Event) { + scrollSubject.next(evt); + } + }; + scrollParentNodes.forEach((scrollParentNode) => { + scrollParentNode.addEventListener('scroll', eventListenerObject); + }); + window.addEventListener('resize', eventListenerObject); + const shared = scrollSubject.pipe( + finalize(() => { + scrollParentNodes.forEach((scrollParentNode) => { + scrollParentNode.removeEventListener('scroll', eventListenerObject); + }); + window.removeEventListener('resize', eventListenerObject); + }), + share() + ); + return shared; +} + +const scrollRegex = /(auto|scroll)/; + +function parentNodes(node: Node, nodes: Node[]): Node[] { + if (node.parentNode === null) { + return nodes; + } + return parentNodes(node.parentNode, nodes.concat([node])); +} + +function style(el: Element, prop: string): string { + return getComputedStyle(el, null).getPropertyValue(prop); +} + +function overflow(el: Element): string { + return style(el, 'overflow') + style(el, 'overflow-y') + style(el, 'overflow-x'); +} + +function isScrollNode(node: Node): boolean { + if (node instanceof Element) { + return scrollRegex.test(overflow(node)); + } else { + return false; + } +} + +function scrollParents(node: Node): Node[] { + if (!(node instanceof HTMLElement || node instanceof SVGElement)) { + return []; + } + const scrollParentNodes = []; + const nodeParents = parentNodes(node, []); + nodeParents.forEach((nodeParent) => { + if (isScrollNode(nodeParent)) { + scrollParentNodes.push(nodeParent); + } + }); + if (document.scrollingElement) { + scrollParentNodes.push(document.scrollingElement); + } else if (document.documentElement) { + scrollParentNodes.push(document.documentElement); + } + return scrollParentNodes; +} diff --git a/ui-ngx/src/app/modules/home/home-routing.module.ts b/ui-ngx/src/app/modules/home/home-routing.module.ts new file mode 100644 index 0000000000..d2a6332c8c --- /dev/null +++ b/ui-ngx/src/app/modules/home/home-routing.module.ts @@ -0,0 +1,45 @@ +/// +/// Copyright © 2016-2019 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 { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { HomeComponent } from './home.component'; +import { AuthGuard } from '@core/guards/auth.guard'; +import { StoreModule } from '@ngrx/store'; + +const routes: Routes = [ + { path: '', + component: HomeComponent, + data: { + title: 'home.home', + breadcrumb: { + skip: true + } + }, + canActivate: [AuthGuard], + canActivateChild: [AuthGuard], + loadChildren: './pages/home-pages.module#HomePagesModule' + } +]; + +@NgModule({ + imports: [ + StoreModule, + RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class HomeRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/home.component.html b/ui-ngx/src/app/modules/home/home.component.html new file mode 100644 index 0000000000..c4d387464e --- /dev/null +++ b/ui-ngx/src/app/modules/home/home.component.html @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + menu + + + + + {{ isFullscreen() ? 'fullscreen_exit' : 'fullscreen' }} + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/home.component.scss b/ui-ngx/src/app/modules/home/home.component.scss new file mode 100644 index 0000000000..7d26343517 --- /dev/null +++ b/ui-ngx/src/app/modules/home/home.component.scss @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2019 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. + */ +:host { + display: flex; + width: 100%; + height: 100%; + mat-sidenav-container { + flex: 1; + } + mat-sidenav.tb-site-sidenav { + width: 250px; + @media (max-width:456px) { + width: calc(100% - 56px); + } + .tb-nav-header { + z-index: 2; + flex-shrink: 0; + white-space: nowrap; + .tb-nav-header-toolbar { + min-height: 64px; + height: inherit; + z-index: 2; + flex-shrink: 0; + white-space: nowrap; + border-bottom: 1px solid rgba(0, 0, 0, .12); + & > div { + height: 64px; + .tb-logo-title { + width: auto; + height: 36px; + margin: auto; + } + } + } + } + .tb-side-menu-toolbar { + overflow-y: auto; + height: inherit; + padding: 0; + } + } + .tb-primary-toolbar { + z-index: 2; + h1 { + font-size: 24px !important; + font-weight: 400 !important; + } + } + .tb-main-content { + overflow: auto; + position: relative; + } +} diff --git a/ui-ngx/src/app/modules/home/home.component.ts b/ui-ngx/src/app/modules/home/home.component.ts new file mode 100644 index 0000000000..13462e42e2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/home.component.ts @@ -0,0 +1,114 @@ +/// +/// Copyright © 2016-2019 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 { Component, OnInit, ViewChild } from '@angular/core'; +import { Observable } from 'rxjs'; +import { select, Store } from '@ngrx/store'; +import { map, mergeMap, take } from 'rxjs/operators'; + +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; +import { User } from '@shared/models/user.model'; +import { PageComponent } from '@shared/components/page.component'; +import { AppState } from '@core/core.state'; +import { AuthService } from '@core/auth/auth.service'; +import { UserService } from '@core/http/user.service'; +import { MenuService } from '@core/services/menu.service'; +import { selectAuthUser, selectUserDetails } from '@core/auth/auth.selectors'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { Router } from '@angular/router'; +import * as screenfull from 'screenfull'; +import { MatSidenav } from '@angular/material'; + +@Component({ + selector: 'tb-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'] +}) +export class HomeComponent extends PageComponent implements OnInit { + + sidenavMode = 'side'; + sidenavOpened = true; + + logo = require('../../../assets/logo_title_white.svg'); + + @ViewChild('sidenav', {static: false}) + sidenav: MatSidenav; + + // @ts-ignore + fullscreenEnabled = screenfull.enabled; + + authUser$: Observable; + userDetails$: Observable; + userDetailsString: Observable; + testUser1$: Observable; + testUser2$: Observable; + testUser3$: Observable; + + constructor(protected store: Store, + private authService: AuthService, + private router: Router, + private userService: UserService, private menuService: MenuService, + public breakpointObserver: BreakpointObserver) { + super(store); + } + + ngOnInit() { + + this.authUser$ = this.store.pipe(select(selectAuthUser)); + this.userDetails$ = this.store.pipe(select(selectUserDetails)); + this.userDetailsString = this.userDetails$.pipe(map((user: User) => { + return JSON.stringify(user); + })); + + const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); + this.sidenavMode = isGtSm ? 'side' : 'over'; + this.sidenavOpened = isGtSm; + + this.breakpointObserver + .observe(MediaBreakpoints['gt-sm']) + .subscribe((state: BreakpointState) => { + if (state.matches) { + this.sidenavMode = 'side'; + this.sidenavOpened = true; + } else { + this.sidenavMode = 'over'; + this.sidenavOpened = false; + } + } + ); + } + + sidenavClicked() { + if (this.sidenavMode === 'over') { + this.sidenav.toggle(); + } + } + + toggleFullscreen() { + // @ts-ignore + if (screenfull.enabled) { + // @ts-ignore + screenfull.toggle(); + } + } + + isFullscreen() { + // @ts-ignore + return screenfull.isFullscreen; + } + +} diff --git a/ui-ngx/src/app/modules/home/home.module.ts b/ui-ngx/src/app/modules/home/home.module.ts new file mode 100644 index 0000000000..2db95ff74b --- /dev/null +++ b/ui-ngx/src/app/modules/home/home.module.ts @@ -0,0 +1,41 @@ +/// +/// Copyright © 2016-2019 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { HomeRoutingModule } from './home-routing.module'; +import { HomeComponent } from './home.component'; +import { SharedModule } from '@app/shared/shared.module'; +import { MenuLinkComponent } from '@modules/home/menu/menu-link.component'; +import { MenuToggleComponent } from '@modules/home/menu/menu-toggle.component'; +import { SideMenuComponent } from '@modules/home/menu/side-menu.component'; + +@NgModule({ + declarations: + [ + HomeComponent, + MenuLinkComponent, + MenuToggleComponent, + SideMenuComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeRoutingModule + ] +}) +export class HomeModule { } diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.html b/ui-ngx/src/app/modules/home/menu/menu-link.component.html new file mode 100644 index 0000000000..c229a57c39 --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.html @@ -0,0 +1,23 @@ + + + {{section.icon}} + + {{section.name | translate}} + diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.scss b/ui-ngx/src/app/modules/home/menu/menu-link.component.scss new file mode 100644 index 0000000000..dfbd362f33 --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2019 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. + */ +:host { + +} diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts new file mode 100644 index 0000000000..82ba35f4aa --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2019 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 { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { MenuSection } from '@core/services/menu.models'; + +@Component({ + selector: 'tb-menu-link', + templateUrl: './menu-link.component.html', + styleUrls: ['./menu-link.component.scss'] +}) +export class MenuLinkComponent implements OnInit { + + @Input() section: MenuSection; + + constructor() { + } + + ngOnInit() { + } + +} diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html new file mode 100644 index 0000000000..6444eaf8fb --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html @@ -0,0 +1,31 @@ + + + {{section.icon}} + + {{section.name | translate}} + + + + + + + diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss new file mode 100644 index 0000000000..dfbd362f33 --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2019 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. + */ +:host { + +} diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts new file mode 100644 index 0000000000..76c4002d8b --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts @@ -0,0 +1,47 @@ +/// +/// Copyright © 2016-2019 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 { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { MenuSection } from '@core/services/menu.models'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'tb-menu-toggle', + templateUrl: './menu-toggle.component.html', + styleUrls: ['./menu-toggle.component.scss'] +}) +export class MenuToggleComponent implements OnInit { + + @Input() section: MenuSection; + + constructor(private router: Router) { + } + + ngOnInit() { + } + + sectionActive(): boolean { + return this.router.isActive(this.section.path, false); + } + + sectionHeight(): string { + if (this.router.isActive(this.section.path, false)) { + return this.section.height; + } else { + return '0px'; + } + } +} diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.html b/ui-ngx/src/app/modules/home/menu/side-menu.component.html new file mode 100644 index 0000000000..cbf8e27240 --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.html @@ -0,0 +1,23 @@ + + + + + + + diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.scss b/ui-ngx/src/app/modules/home/menu/side-menu.component.scss new file mode 100644 index 0000000000..53100c456e --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.scss @@ -0,0 +1,114 @@ +/** + * Copyright © 2016-2019 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. + */ +:host { + width: 100%; +} + +:host ::ng-deep { + + .tb-side-menu, + .tb-side-menu ul { + padding: 0; + margin-top: 0; + list-style: none; + } + + .tb-side-menu > li { + border-bottom: 1px solid rgba(0, 0, 0, .12); + } + + button { + display: flex; + width: 100%; + max-height: 40px; + padding: 0 16px; + margin: 0; + overflow: hidden; + line-height: 40px; + color: inherit; + text-align: left; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + border-radius: 0; + &:hover { + background-color: rgba(255,255,255,0.08); + } + + .mat-button-wrapper { + width: 100%; + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + } + + button.tb-active { + font-weight: 500; + background-color: rgba(255, 255, 255, .15); + } + + span.tb-toggle-icon { + padding-top: 12px; + padding-bottom: 12px; + } + + mat-icon { + margin-right: 8px; + margin-left: 0; + } + + .tb-menu-toggle-list button { + padding: 0 16px 0 32px; + font-weight: 500; + text-transform: none; + text-rendering: optimizeLegibility; + } + + .tb-button-toggle .tb-toggle-icon { + display: inline-block; + width: 15px; + margin: auto 0 auto auto; + background-size: 100% auto; + + transition: transform .3s, ease-in-out; + } + + .tb-button-toggle .tb-toggle-icon.tb-toggled { + transform: rotateZ(180deg); + } + + .tb-menu-toggle-list { + position: relative; + z-index: 1; + overflow: hidden; + + transition: .75s cubic-bezier(.35, 0, .25, 1); + + transition-property: height; + + button { + padding: 0 16px 0 32px; + font-weight: 500; + text-transform: none; + text-rendering: optimizeLegibility; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.ts b/ui-ngx/src/app/modules/home/menu/side-menu.component.ts new file mode 100644 index 0000000000..60b1952432 --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2019 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 { Component, OnInit } from '@angular/core'; +import { MenuService } from '@core/services/menu.service'; + +@Component({ + selector: 'tb-side-menu', + templateUrl: './side-menu.component.html', + styleUrls: ['./side-menu.component.scss'] +}) +export class SideMenuComponent implements OnInit { + + menuSections$ = this.menuService.menuSections(); + + constructor(private menuService: MenuService) { + } + + ngOnInit() { + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts new file mode 100644 index 0000000000..1227f9619b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2019 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 {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; + +import {HomeLinksComponent} from './home-links.component'; +import {Authority} from '@shared/models/authority.enum'; + +const routes: Routes = [ + { + path: 'home', + component: HomeLinksComponent, + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'home.home', + breadcrumb: { + label: 'home.home', + icon: 'home' + } + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class HomeLinksRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html new file mode 100644 index 0000000000..43724776db --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html @@ -0,0 +1,37 @@ + + + + + + {{section.name}} + + + + + + {{place.icon}} + + {{place.name}} + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss new file mode 100644 index 0000000000..6d103ff1a9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss @@ -0,0 +1,74 @@ +/** + * Copyright © 2016-2019 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 '../../../../../scss/constants'; + +:host ::ng-deep { + .tb-home-links { + .mat-headline { + font-size: 20px; + @media #{$mat-gt-xmd} { + font-size: 24px; + } + } + mat-card { + padding: 0; + margin: 8px; + mat-card-title { + margin: 0; + padding: 24px 16px 16px; + } + mat-card-title+mat-card-content { + padding-top: 0; + } + mat-card-content { + padding: 16px; + } + } + button.tb-card-button { + width: 100%; + height: 100%; + max-width: 240px; + .mat-button-wrapper { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + mat-icon { + margin: auto; + } + span { + height: 18px; + min-height: 18px; + max-height: 18px; + padding: 0 0 20px 0; + margin: auto; + font-size: 18px; + font-weight: 400; + line-height: 18px; + white-space: normal; + } + } + &.mat-raised-button.mat-primary { + .mat-ripple-element { + opacity: 0.3; + background-color: rgba(255, 255, 255, 0.3); + } + } + } + } +} + diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts new file mode 100644 index 0000000000..f8f538f52b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts @@ -0,0 +1,69 @@ +/// +/// Copyright © 2016-2019 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 { Component, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { MenuService } from '@core/services/menu.service'; +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { HomeSection } from '@core/services/menu.models'; + +@Component({ + selector: 'tb-home-links', + templateUrl: './home-links.component.html', + styleUrls: ['./home-links.component.scss'] +}) +export class HomeLinksComponent implements OnInit { + + homeSections$ = this.menuService.homeSections(); + + cols = 2; + + constructor(private menuService: MenuService, + public breakpointObserver: BreakpointObserver) { + } + + ngOnInit() { + this.updateColumnCount(); + this.breakpointObserver + .observe([MediaBreakpoints.lg, MediaBreakpoints['gt-lg']]) + .subscribe((state: BreakpointState) => this.updateColumnCount()); + } + + private updateColumnCount() { + this.cols = 2; + if (this.breakpointObserver.isMatched(MediaBreakpoints.lg)) { + this.cols = 3; + } + if (this.breakpointObserver.isMatched(MediaBreakpoints['gt-lg'])) { + this.cols = 4; + } + } + + sectionColspan(section: HomeSection): number { + if (this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm'])) { + let colspan = this.cols; + if (section && section.places && section.places.length <= colspan) { + colspan = section.places.length; + } + return colspan; + } else { + return 2; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.module.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links.module.ts new file mode 100644 index 0000000000..66e11f093b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.module.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2019 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { HomeLinksRoutingModule } from './home-links-routing.module'; +import { HomeLinksComponent} from './home-links.component'; +import { SharedModule } from '@app/shared/shared.module'; + +@NgModule({ + declarations: + [ + HomeLinksComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeLinksRoutingModule + ] +}) +export class HomeLinksModule { } diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts new file mode 100644 index 0000000000..cbd37fcc65 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -0,0 +1,36 @@ +/// +/// Copyright © 2016-2019 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 { NgModule } from '@angular/core'; + +// import { AdminModule } from './admin/admin.module'; +import { HomeLinksModule } from './home-links/home-links.module'; +// import { ProfileModule } from './profile/profile.module'; +// import { CustomerModule } from '@modules/home/pages/customer/customer.module'; +// import { AuditLogModule } from '@modules/home/pages/audit-log/audit-log.module'; +// import { UserModule } from '@modules/home/pages/user/user.module'; + +@NgModule({ + exports: [ +// AdminModule, + HomeLinksModule, +// ProfileModule, +// CustomerModule, +// AuditLogModule, +// UserModule + ] +}) +export class HomePagesModule { } diff --git a/ui-ngx/src/app/modules/login/login-routing.module.ts b/ui-ngx/src/app/modules/login/login-routing.module.ts new file mode 100644 index 0000000000..3af7f4dd1d --- /dev/null +++ b/ui-ngx/src/app/modules/login/login-routing.module.ts @@ -0,0 +1,69 @@ +/// +/// Copyright © 2016-2019 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 { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { LoginComponent } from './pages/login/login.component'; +import { AuthGuard } from '../../core/guards/auth.guard'; +import { ResetPasswordRequestComponent } from '@modules/login/pages/login/reset-password-request.component'; +import { ResetPasswordComponent } from '@modules/login/pages/login/reset-password.component'; +import { CreatePasswordComponent } from '@modules/login/pages/login/create-password.component'; + +const routes: Routes = [ + { + path: 'login', + component: LoginComponent, + data: { + title: 'login.login', + module: 'public' + }, + canActivate: [AuthGuard] + }, + { + path: 'login/resetPasswordRequest', + component: ResetPasswordRequestComponent, + data: { + title: 'login.request-password-reset', + module: 'public' + }, + canActivate: [AuthGuard] + }, + { + path: 'login/resetPassword', + component: ResetPasswordComponent, + data: { + title: 'login.reset-password', + module: 'public' + }, + canActivate: [AuthGuard] + }, + { + path: 'login/createPassword', + component: CreatePasswordComponent, + data: { + title: 'login.create-password', + module: 'public' + }, + canActivate: [AuthGuard] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class LoginRoutingModule { } diff --git a/ui-ngx/src/app/modules/login/login.module.ts b/ui-ngx/src/app/modules/login/login.module.ts new file mode 100644 index 0000000000..5ed9e66eb6 --- /dev/null +++ b/ui-ngx/src/app/modules/login/login.module.ts @@ -0,0 +1,40 @@ +/// +/// Copyright © 2016-2019 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { LoginRoutingModule } from './login-routing.module'; +import { LoginComponent } from './pages/login/login.component'; +import { SharedModule } from '@app/shared/shared.module'; +import { ResetPasswordRequestComponent } from '@modules/login/pages/login/reset-password-request.component'; +import { ResetPasswordComponent } from '@modules/login/pages/login/reset-password.component'; +import { CreatePasswordComponent } from '@modules/login/pages/login/create-password.component'; + +@NgModule({ + declarations: [ + LoginComponent, + ResetPasswordRequestComponent, + ResetPasswordComponent, + CreatePasswordComponent + ], + imports: [ + CommonModule, + SharedModule, + LoginRoutingModule + ] +}) +export class LoginModule { } diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html new file mode 100644 index 0000000000..739bb9cf4e --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html @@ -0,0 +1,56 @@ + + + + + login.create-password + + + + + + + + + + + common.password + + lock + + + login.password-again + + lock + + + + {{ 'login.create-password' | translate }} + + + {{ 'action.cancel' | translate }} + + + + + + + + diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss b/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss new file mode 100644 index 0000000000..8f10478802 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2019 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 '../../../../../scss/constants'; + +:host { + display: flex; + flex: 1 1 0%; + .tb-create-password-content { + background-color: #eee; + .tb-create-password-card { + @media #{$mat-gt-sm} { + width: 450px !important; + } + } + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts new file mode 100644 index 0000000000..2e471db8a3 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts @@ -0,0 +1,76 @@ +/// +/// Copyright © 2016-2019 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 { Component, OnDestroy, OnInit } from '@angular/core'; +import { AuthService } from '../../../../core/auth/auth.service'; +import { LoginRequest } from '../../../../shared/models/login.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '../../../../core/core.state'; +import { PageComponent } from '../../../../shared/components/page.component'; +import { FormBuilder } from '@angular/forms'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; +import { Observable, Subscription } from 'rxjs'; +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'tb-create-password', + templateUrl: './create-password.component.html', + styleUrls: ['./create-password.component.scss'] +}) +export class CreatePasswordComponent extends PageComponent implements OnInit, OnDestroy { + + activateToken = ''; + sub: Subscription; + + createPassword = this.fb.group({ + password: [''], + password2: [''] + }); + + constructor(protected store: Store, + private route: ActivatedRoute, + private authService: AuthService, + private translate: TranslateService, + public fb: FormBuilder) { + super(store); + } + + ngOnInit() { + this.sub = this.route + .queryParams + .subscribe(params => { + this.activateToken = params.activateToken || ''; + }); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + this.sub.unsubscribe(); + } + + onCreatePassword() { + if (this.createPassword.get('password').value !== this.createPassword.get('password2').value) { + this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'), + type: 'error' })); + } else { + this.authService.activate( + this.activateToken, + this.createPassword.get('password').value).subscribe(); + } + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.html b/ui-ngx/src/app/modules/login/pages/login/login.component.html new file mode 100644 index 0000000000..5d9a1b7e5d --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.html @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + login.username + + email + + {{ 'user.invalid-email-format' | translate }} + + + + common.password + + lock + + + + {{ 'login.forgot-password' | translate }} + + + + + {{ 'login.login' | translate }} + + + + + + + + diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.scss b/ui-ngx/src/app/modules/login/pages/login/login.component.scss new file mode 100644 index 0000000000..ea75abdce6 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.scss @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2019 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 '../../../../../scss/constants'; + +:host { + display: flex; + flex: 1 1 0%; + .tb-login-content { + margin-top: 36px; + margin-bottom: 76px; + background-color: rgb(250,250,250); + .tb-login-form { + @media #{$mat-gt-sm} { + width: 550px !important; + } + } + } +} + diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.ts b/ui-ngx/src/app/modules/login/pages/login/login.component.ts new file mode 100644 index 0000000000..ca75c1f4b8 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.ts @@ -0,0 +1,54 @@ +/// +/// Copyright © 2016-2019 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 { Component, OnInit } from '@angular/core'; +import { AuthService } from '../../../../core/auth/auth.service'; +import { LoginRequest } from '../../../../shared/models/login.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '../../../../core/core.state'; +import { PageComponent } from '../../../../shared/components/page.component'; +import { FormBuilder } from '@angular/forms'; + +@Component({ + selector: 'tb-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'] +}) +export class LoginComponent extends PageComponent implements OnInit { + + loginFormGroup = this.fb.group(new LoginRequest('', '')); + + constructor(protected store: Store, + private authService: AuthService, + public fb: FormBuilder) { + super(store); + } + + ngOnInit() { + } + + login(): void { + if (this.loginFormGroup.valid) { + this.authService.login(this.loginFormGroup.value).subscribe(); + } else { + Object.keys(this.loginFormGroup.controls).forEach(field => { + const control = this.loginFormGroup.get(field); + control.markAsTouched({onlySelf: true}); + }); + } + } + +} diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html new file mode 100644 index 0000000000..e1f5e85036 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html @@ -0,0 +1,54 @@ + + + + + login.request-password-reset + + + + + + + + + + + login.email + + email + + {{ 'user.invalid-email-format' | translate }} + + + + + {{ 'login.request-password-reset' | translate }} + + + {{ 'action.cancel' | translate }} + + + + + + + + diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss new file mode 100644 index 0000000000..5f9c8b0258 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2019 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 '../../../../../scss/constants'; + +:host { + display: flex; + flex: 1 1 0%; + .tb-request-password-reset-content { + background-color: #eee; + .tb-request-password-reset-card { + @media #{$mat-gt-sm} { + width: 450px !important; + } + } + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts new file mode 100644 index 0000000000..b8ac67df3f --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts @@ -0,0 +1,57 @@ +/// +/// Copyright © 2016-2019 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 { Component, OnInit } from '@angular/core'; +import { AuthService } from '../../../../core/auth/auth.service'; +import { LoginRequest } from '../../../../shared/models/login.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '../../../../core/core.state'; +import { PageComponent } from '../../../../shared/components/page.component'; +import { FormBuilder } from '@angular/forms'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-reset-password-request', + templateUrl: './reset-password-request.component.html', + styleUrls: ['./reset-password-request.component.scss'] +}) +export class ResetPasswordRequestComponent extends PageComponent implements OnInit { + + requestPasswordRequest = this.fb.group({ + email: [''] + }); + + constructor(protected store: Store, + private authService: AuthService, + private translate: TranslateService, + public fb: FormBuilder) { + super(store); + } + + ngOnInit() { + } + + sendResetPasswordLink() { + this.authService.sendResetPasswordLink(this.requestPasswordRequest.get('email').value).subscribe( + () => { + this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.password-link-sent-message'), + type: 'success' })); + } + ); + } + +} diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html new file mode 100644 index 0000000000..6e7ddcee49 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html @@ -0,0 +1,56 @@ + + + + + login.password-reset + + + + + + + + + + + login.new-password + + lock + + + login.new-password-again + + lock + + + + {{ 'login.reset-password' | translate }} + + + {{ 'action.cancel' | translate }} + + + + + + + + diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss new file mode 100644 index 0000000000..4bcec045e9 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2019 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 '../../../../../scss/constants'; + +:host { + display: flex; + flex: 1 1 0%; + .tb-reset-password-content { + background-color: #eee; + .tb-reset-password-card { + @media #{$mat-gt-sm} { + width: 450px !important; + } + } + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts new file mode 100644 index 0000000000..6d97bbe2dd --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts @@ -0,0 +1,76 @@ +/// +/// Copyright © 2016-2019 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 { Component, OnDestroy, OnInit } from '@angular/core'; +import { AuthService } from '../../../../core/auth/auth.service'; +import { LoginRequest } from '../../../../shared/models/login.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '../../../../core/core.state'; +import { PageComponent } from '../../../../shared/components/page.component'; +import { FormBuilder } from '@angular/forms'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { ActivatedRoute } from '@angular/router'; +import { Observable, Subscription } from 'rxjs'; +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'tb-reset-password', + templateUrl: './reset-password.component.html', + styleUrls: ['./reset-password.component.scss'] +}) +export class ResetPasswordComponent extends PageComponent implements OnInit, OnDestroy { + + resetToken = ''; + sub: Subscription; + + resetPassword = this.fb.group({ + newPassword: [''], + newPassword2: [''] + }); + + constructor(protected store: Store, + private route: ActivatedRoute, + private authService: AuthService, + private translate: TranslateService, + public fb: FormBuilder) { + super(store); + } + + ngOnInit() { + this.sub = this.route + .queryParams + .subscribe(params => { + this.resetToken = params.resetToken || ''; + }); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + this.sub.unsubscribe(); + } + + onResetPassword() { + if (this.resetPassword.get('newPassword').value !== this.resetPassword.get('newPassword2').value) { + this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'), + type: 'error' })); + } else { + this.authService.resetPassword( + this.resetToken, + this.resetPassword.get('newPassword').value).subscribe(); + } + } +} diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.html b/ui-ngx/src/app/shared/components/breadcrumb.component.html new file mode 100644 index 0000000000..2a2de92b2b --- /dev/null +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.html @@ -0,0 +1,39 @@ + + + {{ (lastBreadcrumb$ | async).label | translate }} + + + + + + {{ breadcrumb.icon }} + + {{ breadcrumb.label | translate }} + + + + + + {{ breadcrumb.icon }} + + {{ breadcrumb.label | translate }} + + > + + diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.scss b/ui-ngx/src/app/shared/components/breadcrumb.component.scss new file mode 100644 index 0000000000..36ad8fcd91 --- /dev/null +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.scss @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2019 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. + */ +:host { + display: flex; + flex-direction: row; + align-items: center; + min-width: 0; + flex: 1; + + .tb-breadcrumb { + font-size: 18px !important; + font-weight: 400 !important; + + h1, + a, + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + h1 { + font-size: 24px !important; + font-weight: 400 !important; + } + + a { + border: none; + opacity: .75; + transition: opacity .35s; + color: inherit; + text-decoration: none; + } + + a:hover, + a:focus { + text-decoration: none !important; + border: none; + opacity: 1; + } + + .divider { + padding: 0 30px; + } + } +} diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.ts b/ui-ngx/src/app/shared/components/breadcrumb.component.ts new file mode 100644 index 0000000000..3e7fb18adc --- /dev/null +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.ts @@ -0,0 +1,82 @@ +/// +/// Copyright © 2016-2019 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 { Component, OnDestroy, OnInit } from '@angular/core'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { BreadCrumb } from './breadcrumb'; +import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; +import { distinctUntilChanged, filter, map } from 'rxjs/operators'; + +@Component({ + selector: '[tb-breadcrumb]', + templateUrl: './breadcrumb.component.html', + styleUrls: ['./breadcrumb.component.scss'] +}) +export class BreadcrumbComponent implements OnInit, OnDestroy { + + breadcrumbs$: Subject> = new BehaviorSubject>(this.buildBreadCrumbs(this.activatedRoute.snapshot)); + + routerEventsSubscription = this.router.events.pipe( + filter((event) => event instanceof NavigationEnd ), + distinctUntilChanged(), + map( () => this.buildBreadCrumbs(this.activatedRoute.snapshot) ) + ).subscribe(breadcrumns => this.breadcrumbs$.next(breadcrumns) ); + + lastBreadcrumb$ = this.breadcrumbs$.pipe( + map( breadcrumbs => breadcrumbs[breadcrumbs.length - 1]) + ); + + constructor(private router: Router, + private activatedRoute: ActivatedRoute) { + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + if (this.routerEventsSubscription) { + this.routerEventsSubscription.unsubscribe(); + } + } + + + buildBreadCrumbs(route: ActivatedRouteSnapshot, breadcrumbs: Array = []): Array { + let newBreadcrumbs = breadcrumbs; + if (route.routeConfig && route.routeConfig.data) { + const breadcrumbData = route.routeConfig.data.breadcrumb; + if (breadcrumbData && !breadcrumbData.skip) { + const label = breadcrumbData.label || 'home.home'; + const icon = breadcrumbData.icon || 'home'; + const isMdiIcon = icon.startsWith('mdi:'); + const link = [ '/' + route.url.join('') ]; + const queryParams = route.queryParams; + const breadcrumb = { + label, + icon, + isMdiIcon, + link, + queryParams + }; + newBreadcrumbs = [...breadcrumbs, breadcrumb]; + } + } + if (route.firstChild) { + return this.buildBreadCrumbs(route.firstChild, newBreadcrumbs); + } + return newBreadcrumbs; + } + +} diff --git a/ui-ngx/src/app/shared/components/breadcrumb.ts b/ui-ngx/src/app/shared/components/breadcrumb.ts new file mode 100644 index 0000000000..0986bb4715 --- /dev/null +++ b/ui-ngx/src/app/shared/components/breadcrumb.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2019 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 { Params } from '@angular/router'; + +export interface BreadCrumb { + label: string; + icon: string; + isMdiIcon: boolean; + link: any[]; + queryParams: Params; +} + diff --git a/ui-ngx/src/app/shared/components/footer.component.html b/ui-ngx/src/app/shared/components/footer.component.html new file mode 100644 index 0000000000..828d60e573 --- /dev/null +++ b/ui-ngx/src/app/shared/components/footer.component.html @@ -0,0 +1,20 @@ + + + Copyright © {{year}} The ThingsBoard Authors + diff --git a/ui-ngx/src/app/shared/components/footer.component.scss b/ui-ngx/src/app/shared/components/footer.component.scss new file mode 100644 index 0000000000..8962a13327 --- /dev/null +++ b/ui-ngx/src/app/shared/components/footer.component.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2019 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. + */ +.footer-text { + position: absolute; + width: 100%; + bottom: 20px; + margin: 0; + left: 0; + line-height: 20px; + text-align: center; + small { + font-size: 14px; + color: #98a6ad; + } +} diff --git a/ui-ngx/src/app/shared/components/footer.component.ts b/ui-ngx/src/app/shared/components/footer.component.ts new file mode 100644 index 0000000000..3bebc95a2f --- /dev/null +++ b/ui-ngx/src/app/shared/components/footer.component.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2019 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 { Component } from '@angular/core'; + +@Component({ + selector: 'tb-footer', + templateUrl: './footer.component.html', + styleUrls: ['./footer.component.scss'] +}) +export class FooterComponent { + + year = new Date().getFullYear(); + +} diff --git a/ui-ngx/src/app/shared/components/fullscreen.directive.ts b/ui-ngx/src/app/shared/components/fullscreen.directive.ts new file mode 100644 index 0000000000..a738ad4654 --- /dev/null +++ b/ui-ngx/src/app/shared/components/fullscreen.directive.ts @@ -0,0 +1,99 @@ +/// +/// Copyright © 2016-2019 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 { + Directive, + ElementRef, + EventEmitter, + Input, + Output, + ViewContainerRef +} from '@angular/core'; +import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; + +@Directive({ + selector: '[tb-fullscreen]' +}) +export class FullscreenDirective { + + fullscreenValue = false; + + private overlayRef: OverlayRef; + private parentElement: HTMLElement; + + @Input() + set fullscreen(fullscreen: boolean) { + if (this.fullscreenValue !== fullscreen) { + this.fullscreenValue = fullscreen; + if (this.fullscreenValue) { + this.enterFullscreen(); + } else { + this.exitFullscreen(); + } + } + } + + @Output() + fullscreenChanged = new EventEmitter(); + + constructor(public elementRef: ElementRef, + private viewContainerRef: ViewContainerRef, + private overlay: Overlay) { + + } + + enterFullscreen() { + this.parentElement = this.elementRef.nativeElement.parentElement; + this.parentElement.removeChild(this.elementRef.nativeElement); + this.elementRef.nativeElement.classList.add('tb-fullscreen'); + const position = this.overlay.position(); + const config = new OverlayConfig({ + hasBackdrop: false, + panelClass: 'tb-fullscreen-parent' + }); + config.minWidth = '100%'; + config.minHeight = '100%'; + config.positionStrategy = position.global().top('0%').left('0%') + .right('0%').bottom('0%'); + + this.overlayRef = this.overlay.create(config); + this.overlayRef.attach(new EmptyPortal()); + this.overlayRef.overlayElement.append( this.elementRef.nativeElement ); + this.fullscreenChanged.emit(true); + } + + exitFullscreen() { + if (this.parentElement) { + this.overlayRef.overlayElement.removeChild( this.elementRef.nativeElement ); + this.parentElement.append( this.elementRef.nativeElement ); + this.parentElement = null; + } + this.elementRef.nativeElement.classList.remove('tb-fullscreen'); + this.overlayRef.dispose(); + this.fullscreenChanged.emit(false); + } + +} + +class EmptyPortal extends ComponentPortal { + + constructor() { + super(TbAnchorComponent); + } + +} diff --git a/ui-ngx/src/app/shared/components/help.component.html b/ui-ngx/src/app/shared/components/help.component.html new file mode 100644 index 0000000000..2f319e097c --- /dev/null +++ b/ui-ngx/src/app/shared/components/help.component.html @@ -0,0 +1,24 @@ + + + help + diff --git a/ui-ngx/src/app/shared/components/help.component.ts b/ui-ngx/src/app/shared/components/help.component.ts new file mode 100644 index 0000000000..0bc85834d4 --- /dev/null +++ b/ui-ngx/src/app/shared/components/help.component.ts @@ -0,0 +1,40 @@ +/// +/// Copyright © 2016-2019 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 { Component, Input } from '@angular/core'; +import { HelpLinks } from '@shared/models/constants'; + +@Component({ + selector: '[tb-help]', + templateUrl: './help.component.html' +}) +export class HelpComponent { + + // tslint:disable-next-line:no-input-rename + @Input('tb-help') helpLinkId: string; + + gotoHelpPage(): void { + let helpUrl = HelpLinks.linksMap[this.helpLinkId]; + if (!helpUrl && this.helpLinkId && + (this.helpLinkId.startsWith('http://') || this.helpLinkId.startsWith('https://'))) { + helpUrl = this.helpLinkId; + } + if (helpUrl) { + window.open(helpUrl, '_blank'); + } + } + +} diff --git a/ui-ngx/src/app/shared/components/logo.component.html b/ui-ngx/src/app/shared/components/logo.component.html new file mode 100644 index 0000000000..e734b23b34 --- /dev/null +++ b/ui-ngx/src/app/shared/components/logo.component.html @@ -0,0 +1,19 @@ + + diff --git a/ui-ngx/src/app/shared/components/logo.component.scss b/ui-ngx/src/app/shared/components/logo.component.scss new file mode 100644 index 0000000000..c72149fa1e --- /dev/null +++ b/ui-ngx/src/app/shared/components/logo.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2019 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. + */ +:host-context(.login-logo) { + img.tb-logo-title { + width: 280px; + height: 60px; + text-decoration: none; + cursor: pointer; + border: none; + transform: none; + + &:focus { + outline: 0; + } + } +} diff --git a/ui-ngx/src/app/shared/components/logo.component.ts b/ui-ngx/src/app/shared/components/logo.component.ts new file mode 100644 index 0000000000..40b91925fa --- /dev/null +++ b/ui-ngx/src/app/shared/components/logo.component.ts @@ -0,0 +1,32 @@ +/// +/// Copyright © 2016-2019 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 { Component } from '@angular/core'; + +@Component({ + selector: 'tb-logo', + templateUrl: './logo.component.html', + styleUrls: ['./logo.component.scss'] +}) +export class LogoComponent { + + logo = require('../../../assets/logo_title_white.svg'); + + gotoThingsboard(): void { + window.open('https://thingsboard.io', '_blank'); + } + +} diff --git a/ui-ngx/src/app/shared/components/page.component.ts b/ui-ngx/src/app/shared/components/page.component.ts new file mode 100644 index 0000000000..3ed33e3e5f --- /dev/null +++ b/ui-ngx/src/app/shared/components/page.component.ts @@ -0,0 +1,56 @@ +/// +/// Copyright © 2016-2019 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 { OnDestroy } from '@angular/core'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '../../core/core.state'; +import { Observable, Subscription } from 'rxjs'; +import { selectIsLoading } from '../../core/interceptors/load.selectors'; +import { delay } from 'rxjs/operators'; +import { AbstractControl } from '@angular/forms'; + +export abstract class PageComponent implements OnDestroy { + + isLoading$: Observable; + loadingSubscription: Subscription; + disabledOnLoadFormControls: Array = []; + + protected constructor(protected store: Store) { + this.isLoading$ = this.store.pipe(delay(0), select(selectIsLoading), delay(100)); + } + + protected registerDisableOnLoadFormControl(control: AbstractControl) { + this.disabledOnLoadFormControls.push(control); + if (!this.loadingSubscription) { + this.loadingSubscription = this.isLoading$.subscribe((isLoading) => { + for (const formControl of this.disabledOnLoadFormControls) { + if (isLoading) { + formControl.disable({emitEvent: false}); + } else { + formControl.enable({emitEvent: false}); + } + } + }); + } + } + + ngOnDestroy(): void { + if (this.loadingSubscription) { + this.loadingSubscription.unsubscribe(); + } + } + +} diff --git a/ui-ngx/src/app/shared/components/snack-bar-component.html b/ui-ngx/src/app/shared/components/snack-bar-component.html new file mode 100644 index 0000000000..4dabc33d75 --- /dev/null +++ b/ui-ngx/src/app/shared/components/snack-bar-component.html @@ -0,0 +1,26 @@ + + + + {{ 'action.close' | translate }} + diff --git a/ui-ngx/src/app/shared/components/snack-bar-component.scss b/ui-ngx/src/app/shared/components/snack-bar-component.scss new file mode 100644 index 0000000000..32926b5748 --- /dev/null +++ b/ui-ngx/src/app/shared/components/snack-bar-component.scss @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2019 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. + */ +:host { + display: inline-block; + pointer-events: all; + .tb-toast { + box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12); + color: #fff; + font-size: 18px; + border-radius: 4px; + padding: 0px 18px; + margin: 8px; + .toast-text { + padding: 0px 6px; + width: 100%; + } + button { + margin: 6px 0px 6px 12px; + } + &.info-toast { + background: #323232; + } + &.error-toast { + background: #800000; + } + &.success-toast { + background: #008000; + } + } +} diff --git a/ui-ngx/src/app/shared/components/tb-anchor.component.ts b/ui-ngx/src/app/shared/components/tb-anchor.component.ts new file mode 100644 index 0000000000..39f4a8b27a --- /dev/null +++ b/ui-ngx/src/app/shared/components/tb-anchor.component.ts @@ -0,0 +1,25 @@ +/// +/// Copyright © 2016-2019 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 { Component, ViewContainerRef } from '@angular/core'; + +@Component({ + selector: 'tb-anchor', + template: '' +}) +export class TbAnchorComponent { + constructor(public viewContainerRef: ViewContainerRef) { } +} diff --git a/ui-ngx/src/app/shared/components/tb-checkbox.component.html b/ui-ngx/src/app/shared/components/tb-checkbox.component.html new file mode 100644 index 0000000000..d668c08ace --- /dev/null +++ b/ui-ngx/src/app/shared/components/tb-checkbox.component.html @@ -0,0 +1,24 @@ + + + + diff --git a/ui-ngx/src/app/shared/components/tb-checkbox.component.ts b/ui-ngx/src/app/shared/components/tb-checkbox.component.ts new file mode 100644 index 0000000000..744ad821a6 --- /dev/null +++ b/ui-ngx/src/app/shared/components/tb-checkbox.component.ts @@ -0,0 +1,74 @@ +/// +/// Copyright © 2016-2019 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 { Component, EventEmitter, forwardRef, Input, Output } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +@Component({ + selector: 'tb-checkbox', + templateUrl: './tb-checkbox.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TbCheckboxComponent), + multi: true + } + ] +}) +export class TbCheckboxComponent implements ControlValueAccessor { + + innerValue: boolean; + + @Input() disabled: boolean; + @Input() trueValue: any = true; + @Input() falseValue: any = false; + @Output() valueChange = new EventEmitter(); + + private propagateChange = (_: any) => {}; + + onHostChange(ev) { + this.propagateChange(ev.checked ? this.trueValue : this.falseValue); + } + + modelChange($event) { + if ($event) { + this.innerValue = true; + this.valueChange.emit(this.trueValue); + } else { + this.innerValue = false; + this.valueChange.emit(this.falseValue); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(obj: any): void { + if (obj === this.trueValue) { + this.innerValue = true; + } else { + this.innerValue = false; + } + } +} diff --git a/ui-ngx/src/app/shared/components/toast.directive.ts b/ui-ngx/src/app/shared/components/toast.directive.ts new file mode 100644 index 0000000000..1d6c9ed24d --- /dev/null +++ b/ui-ngx/src/app/shared/components/toast.directive.ts @@ -0,0 +1,154 @@ +/// +/// Copyright © 2016-2019 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 { + AfterViewInit, + Component, + Directive, + ElementRef, + Inject, Input, + OnDestroy, + ViewContainerRef +} from '@angular/core'; +import { + MAT_SNACK_BAR_DATA, + MatSnackBar, + MatSnackBarConfig, + MatSnackBarRef +} from '@angular/material'; +import { NotificationMessage } from '@app/core/notification/notification.models'; +import { onParentScrollOrWindowResize } from '@app/core/utils'; +import { Subscription } from 'rxjs'; +import { NotificationService } from '@app/core/services/notification.service'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { MediaBreakpoints } from '@shared/models/constants'; + +@Directive({ + selector: '[tb-toast]' +}) +export class ToastDirective implements AfterViewInit, OnDestroy { + + @Input() + toastTarget = 'root'; + + private notificationSubscription: Subscription = null; + + constructor(public elementRef: ElementRef, + public viewContainerRef: ViewContainerRef, + private notificationService: NotificationService, + public snackBar: MatSnackBar, + private breakpointObserver: BreakpointObserver) { + } + + ngAfterViewInit(): void { + const toastComponent = this; + + this.notificationSubscription = this.notificationService.getNotification().subscribe( + (notificationMessage) => { + if (notificationMessage && notificationMessage.message) { + const target = notificationMessage.target || 'root'; + if (this.toastTarget === target) { + const data = { + parent: this.elementRef, + notification: notificationMessage + }; + const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); + const config: MatSnackBarConfig = { + horizontalPosition: notificationMessage.horizontalPosition || 'left', + verticalPosition: !isGtSm ? 'bottom' : (notificationMessage.verticalPosition || 'top'), + viewContainerRef: toastComponent.viewContainerRef, + duration: notificationMessage.duration, + data + }; + this.snackBar.openFromComponent(TbSnackBarComponent, config); + } + } + } + ); + } + + ngOnDestroy(): void { + if (this.notificationSubscription) { + this.notificationSubscription.unsubscribe(); + } + } +} + +@Component({ + selector: 'tb-snack-bar-component', + templateUrl: 'snack-bar-component.html', + styleUrls: ['snack-bar-component.scss'] +}) +export class TbSnackBarComponent implements AfterViewInit, OnDestroy { + private parentEl: HTMLElement; + private snackBarContainerEl: HTMLElement; + private parentScrollSubscription: Subscription = null; + public notification: NotificationMessage; + constructor(@Inject(MAT_SNACK_BAR_DATA) public data: any, private elementRef: ElementRef, + public snackBarRef: MatSnackBarRef) { + this.notification = data.notification; + } + + ngAfterViewInit() { + this.parentEl = this.data.parent.nativeElement; + this.snackBarContainerEl = this.elementRef.nativeElement.parentNode; + this.snackBarContainerEl.style.position = 'absolute'; + this.updateContainerRect(); + this.updatePosition(this.snackBarRef.containerInstance.snackBarConfig); + const snackBarComponent = this; + this.parentScrollSubscription = onParentScrollOrWindowResize(this.parentEl).subscribe(() => { + snackBarComponent.updateContainerRect(); + }); + } + + updatePosition(config: MatSnackBarConfig) { + const isRtl = config.direction === 'rtl'; + const isLeft = (config.horizontalPosition === 'left' || + (config.horizontalPosition === 'start' && !isRtl) || + (config.horizontalPosition === 'end' && isRtl)); + const isRight = !isLeft && config.horizontalPosition !== 'center'; + if (isLeft) { + this.snackBarContainerEl.style.justifyContent = 'flex-start'; + } else if (isRight) { + this.snackBarContainerEl.style.justifyContent = 'flex-end'; + } else { + this.snackBarContainerEl.style.justifyContent = 'center'; + } + if (config.verticalPosition === 'top') { + this.snackBarContainerEl.style.alignItems = 'flex-start'; + } else { + this.snackBarContainerEl.style.alignItems = 'flex-end'; + } + } + + ngOnDestroy() { + if (this.parentScrollSubscription) { + this.parentScrollSubscription.unsubscribe(); + } + } + + updateContainerRect() { + const viewportOffset = this.parentEl.getBoundingClientRect(); + this.snackBarContainerEl.style.top = viewportOffset.top + 'px'; + this.snackBarContainerEl.style.left = viewportOffset.left + 'px'; + this.snackBarContainerEl.style.width = viewportOffset.width + 'px'; + this.snackBarContainerEl.style.height = viewportOffset.height + 'px'; + } + + action(): void { + this.snackBarRef.dismissWithAction(); + } +} diff --git a/ui-ngx/src/app/shared/components/user-menu.component.html b/ui-ngx/src/app/shared/components/user-menu.component.html new file mode 100644 index 0000000000..4404091f1c --- /dev/null +++ b/ui-ngx/src/app/shared/components/user-menu.component.html @@ -0,0 +1,41 @@ + + + + account_circle + + {{ userDisplayName }} + {{ authorityName | translate }} + + + + more_vert + + + + + account_circle + home.profile + + + exit_to_app + home.logout + + + + diff --git a/ui-ngx/src/app/shared/components/user-menu.component.scss b/ui-ngx/src/app/shared/components/user-menu.component.scss new file mode 100644 index 0000000000..6db7757a8a --- /dev/null +++ b/ui-ngx/src/app/shared/components/user-menu.component.scss @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2019 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. + */ +:host { + div.tb-user-info { + line-height: 1.5; + + span { + text-align: left; + text-transform: none; + } + + span.tb-user-display-name { + font-size: .8rem; + font-weight: 300; + letter-spacing: .008em; + } + + span.tb-user-authority { + font-size: .8rem; + font-weight: 300; + letter-spacing: .005em; + opacity: .8; + } + + } + + mat-icon.tb-mini-avatar { + width: 36px; + height: 36px; + margin: auto 8px; + font-size: 36px; + } +} + +.tb-user-menu-items { + min-width: 256px; +} diff --git a/ui-ngx/src/app/shared/components/user-menu.component.ts b/ui-ngx/src/app/shared/components/user-menu.component.ts new file mode 100644 index 0000000000..7e41e7d230 --- /dev/null +++ b/ui-ngx/src/app/shared/components/user-menu.component.ts @@ -0,0 +1,116 @@ +/// +/// Copyright © 2016-2019 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 { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { User } from '@shared/models/user.model'; +import { Authority } from '@shared/models/authority.enum'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { selectAuthUser, selectUserDetails } from '@core/auth/auth.selectors'; +import { map } from 'rxjs/operators'; +import { AuthService } from '@core/auth/auth.service'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'tb-user-menu', + templateUrl: './user-menu.component.html', + styleUrls: ['./user-menu.component.scss'] +}) +export class UserMenuComponent implements OnInit, OnDestroy { + + @Input() displayUserInfo: boolean; + + authorities = Authority; + + authority$ = this.store.pipe( + select(selectAuthUser), + map((authUser) => authUser ? authUser.authority : Authority.ANONYMOUS) + ); + + authorityName$ = this.store.pipe( + select(selectUserDetails), + map((user) => this.getAuthorityName(user)) + ); + + userDisplayName$ = this.store.pipe( + select(selectUserDetails), + map((user) => this.getUserDisplayName(user)) + ); + + constructor(private store: Store, + private router: Router, + private authService: AuthService) { + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + } + + getAuthorityName(user: User): string { + let name = null; + if (user) { + const authority = user.authority; + switch (authority) { + case Authority.SYS_ADMIN: + name = 'user.sys-admin'; + break; + case Authority.TENANT_ADMIN: + name = 'user.tenant-admin'; + break; + case Authority.CUSTOMER_USER: + name = 'user.customer'; + break; + } + } + return name; + } + + getUserDisplayName(user: User): string { + let name = ''; + if (user) { + if ((user.firstName && user.firstName.length > 0) || + (user.lastName && user.lastName.length > 0)) { + if (user.firstName) { + name += user.firstName; + } + if (user.lastName) { + if (name.length > 0) { + name += ' '; + } + name += user.lastName; + } + } else { + name = user.email; + } + } + return name; + } + + openProfile(): void { + this.router.navigate(['profile']); + } + + openCustomerProfile(): void { + this.router.navigate(['customerProfile']); + } + + logout(): void { + this.authService.logout(); + } + +} diff --git a/ui-ngx/src/app/shared/models/authority.enum.ts b/ui-ngx/src/app/shared/models/authority.enum.ts new file mode 100644 index 0000000000..568135a237 --- /dev/null +++ b/ui-ngx/src/app/shared/models/authority.enum.ts @@ -0,0 +1,23 @@ +/// +/// Copyright © 2016-2019 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. +/// + +export enum Authority { + SYS_ADMIN = 'SYS_ADMIN', + TENANT_ADMIN = 'TENANT_ADMIN', + CUSTOMER_USER = 'CUSTOMER_USER', + REFRESH_TOKEN = 'REFRESH_TOKEN', + ANONYMOUS = 'ANONYMOUS' +} diff --git a/ui-ngx/src/app/shared/models/base-data.ts b/ui-ngx/src/app/shared/models/base-data.ts new file mode 100644 index 0000000000..945f75a80c --- /dev/null +++ b/ui-ngx/src/app/shared/models/base-data.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2019 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 { EntityId } from '@shared/models/id/entity-id'; +import { HasUUID } from '@shared/models/id/has-uuid'; + +export declare type HasId = EntityId | HasUUID; + +export interface BaseData { + createdTime?: number; + id?: T; + name?: string; +} diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts new file mode 100644 index 0000000000..b57950bef6 --- /dev/null +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -0,0 +1,109 @@ +/// +/// Copyright © 2016-2019 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. +/// + +export const Constants = { + serverErrorCode: { + general: 2, + authentication: 10, + jwtTokenExpired: 11, + tenantTrialExpired: 12, + permissionDenied: 20, + invalidArguments: 30, + badRequestParams: 31, + itemNotFound: 32, + tooManyRequests: 33, + tooManyUpdates: 34 + }, + entryPoints: { + login: '/api/auth/login', + tokenRefresh: '/api/auth/token', + nonTokenBased: '/api/noauth' + } +}; + +export const MediaBreakpoints = { + xs: 'screen and (max-width: 599px)', + sm: 'screen and (min-width: 600px) and (max-width: 959px)', + md: 'screen and (min-width: 960px) and (max-width: 1279px)', + lg: 'screen and (min-width: 1280px) and (max-width: 1919px)', + xl: 'screen and (min-width: 1920px) and (max-width: 5000px)', + 'lt-sm': 'screen and (max-width: 599px)', + 'lt-md': 'screen and (max-width: 959px)', + 'lt-lg': 'screen and (max-width: 1279px)', + 'lt-xl': 'screen and (max-width: 1919px)', + 'gt-xs': 'screen and (min-width: 600px)', + 'gt-sm': 'screen and (min-width: 960px)', + 'gt-md': 'screen and (min-width: 1280px)', + 'gt-lg': 'screen and (min-width: 1920px)', + 'gt-xl': 'screen and (min-width: 5001px)' +}; + +const helpBaseUrl = 'https://thingsboard.io'; + +export const HelpLinks = { + linksMap: { + outgoingMailSettings: helpBaseUrl + '/docs/user-guide/ui/mail-settings', + securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings', + tenants: helpBaseUrl + '/docs/user-guide/ui/tenants', + customers: helpBaseUrl + '/docs/user-guide/customers', + users: helpBaseUrl + '/docs/user-guide/ui/users' + } +}; + +export interface ValueTypeData { + name: string; + icon: string; +} + +export enum ValueType { + STRING = 'STRING', + INTEGER = 'INTEGER', + DOUBLE = 'DOUBLE', + BOOLEAN = 'BOOLEAN' +} + +export const valueTypesMap = new Map( + [ + [ + ValueType.STRING, + { + name: 'value.string', + icon: 'mdi:format-text' + } + ], + [ + ValueType.INTEGER, + { + name: 'value.integer', + icon: 'mdi:numeric' + } + ], + [ + ValueType.DOUBLE, + { + name: 'value.double', + icon: 'mdi:numeric' + } + ], + [ + ValueType.BOOLEAN, + { + name: 'value.boolean', + icon: 'mdi:checkbox-marked-outline' + } + ] + ] +); diff --git a/ui-ngx/src/app/shared/models/contact-based.model.ts b/ui-ngx/src/app/shared/models/contact-based.model.ts new file mode 100644 index 0000000000..40b519426c --- /dev/null +++ b/ui-ngx/src/app/shared/models/contact-based.model.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2019 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 { BaseData, HasId } from './base-data'; + +export interface ContactBased extends BaseData { + country: string; + state: string; + city: string; + address: string; + address2: string; + zip: string; + phone: string; + email: string; +} diff --git a/ui-ngx/src/app/shared/models/customer.model.ts b/ui-ngx/src/app/shared/models/customer.model.ts new file mode 100644 index 0000000000..d7da52711d --- /dev/null +++ b/ui-ngx/src/app/shared/models/customer.model.ts @@ -0,0 +1,25 @@ +/// +/// Copyright © 2016-2019 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 { CustomerId } from '@shared/models/id/customer-id'; +import { ContactBased } from '@shared/models/contact-based.model'; +import {TenantId} from './id/tenant-id'; + +export interface Customer extends ContactBased { + tenantId: TenantId; + title: string; + additionalInfo?: any; +} diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts new file mode 100644 index 0000000000..36747d3380 --- /dev/null +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -0,0 +1,116 @@ +/// +/// Copyright © 2016-2019 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. +/// + +export enum EntityType { + TENANT = 'TENANT', + CUSTOMER = 'CUSTOMER', + USER = 'USER', + DASHBOARD = 'DASHBOARD', + ASSET = 'ASSET', + DEVICE = 'DEVICE', + ALARM = 'ALARM', + RULE_CHAIN = 'RULE_CHAIN', + RULE_NODE = 'RULE_NODE', + ENTITY_VIEW = 'ENTITY_VIEW', + WIDGETS_BUNDLE = 'WIDGETS_BUNDLE', + WIDGET_TYPE = 'WIDGET_TYPE' +} + +export interface EntityTypeTranslation { + type: string; + typePlural: string; + list: string; + nameStartsWith: string; + details: string; + add: string; + noEntities: string; + selectedEntities: string; + search: string; +} + +export interface EntityTypeResource { + helpLinkId: string; +} + +export const entityTypeTranslations = new Map( + [ + [ + EntityType.TENANT, + { + type: 'entity.type-tenant', + typePlural: 'entity.type-tenants', + list: 'entity.list-of-tenants', + nameStartsWith: 'entity.tenant-name-starts-with', + details: 'tenant.tenant-details', + add: 'tenant.add', + noEntities: 'tenant.no-tenants-text', + search: 'tenant.search', + selectedEntities: 'tenant.selected-tenants' + } + ], + [ + EntityType.CUSTOMER, + { + type: 'entity.type-customer', + typePlural: 'entity.type-customers', + list: 'entity.list-of-customers', + nameStartsWith: 'entity.customer-name-starts-with', + details: 'customer.customer-details', + add: 'customer.add', + noEntities: 'customer.no-customers-text', + search: 'customer.search', + selectedEntities: 'customer.selected-customers' + } + ], + [ + EntityType.USER, + { + type: 'entity.type-user', + typePlural: 'entity.type-users', + list: 'entity.list-of-users', + nameStartsWith: 'entity.user-name-starts-with', + details: 'user.user-details', + add: 'user.add', + noEntities: 'user.no-users-text', + search: 'user.search', + selectedEntities: 'user.selected-users' + } + ] + ] +); + +export const entityTypeResources = new Map( + [ + [ + EntityType.TENANT, + { + helpLinkId: 'tenants' + } + ], + [ + EntityType.CUSTOMER, + { + helpLinkId: 'customers' + } + ], + [ + EntityType.USER, + { + helpLinkId: 'users' + } + ] + ] +); diff --git a/ui-ngx/src/app/shared/models/id/customer-id.ts b/ui-ngx/src/app/shared/models/id/customer-id.ts new file mode 100644 index 0000000000..363ea3fecb --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/customer-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2019 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 { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class CustomerId implements EntityId { + entityType = EntityType.CUSTOMER; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/entity-id.ts b/ui-ngx/src/app/shared/models/id/entity-id.ts new file mode 100644 index 0000000000..55bbd2f0c1 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/entity-id.ts @@ -0,0 +1,22 @@ +/// +/// Copyright © 2016-2019 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 { EntityType } from '@shared/models/entity-type.models'; +import { HasUUID } from '@shared/models/id/has-uuid'; + +export interface EntityId extends HasUUID { + entityType: EntityType; +} diff --git a/ui-ngx/src/app/shared/models/id/has-uuid.ts b/ui-ngx/src/app/shared/models/id/has-uuid.ts new file mode 100644 index 0000000000..681c67bc4a --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/has-uuid.ts @@ -0,0 +1,21 @@ +/// +/// Copyright © 2016-2019 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. +/// + +export const NULL_UUID = '13814000-1dd2-11b2-8080-808080808080'; + +export interface HasUUID { + id: string; +} diff --git a/ui-ngx/src/app/shared/models/id/tenant-id.ts b/ui-ngx/src/app/shared/models/id/tenant-id.ts new file mode 100644 index 0000000000..60ae02147b --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/tenant-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2019 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 { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class TenantId implements EntityId { + entityType = EntityType.TENANT; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/user-id.ts b/ui-ngx/src/app/shared/models/id/user-id.ts new file mode 100644 index 0000000000..7a7c2fd1f2 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/user-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2019 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 { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class UserId implements EntityId { + entityType = EntityType.USER; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/login.models.ts b/ui-ngx/src/app/shared/models/login.models.ts new file mode 100644 index 0000000000..9d11359f5a --- /dev/null +++ b/ui-ngx/src/app/shared/models/login.models.ts @@ -0,0 +1,30 @@ +/// +/// Copyright © 2016-2019 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. +/// + +export class LoginRequest { + username: string; + password: string; + + constructor(username: string, password: string) { + this.username = username; + this.password = password; + } +} + +export class LoginResponse { + token: string; + refreshToken: string; +} diff --git a/ui-ngx/src/app/shared/models/page/page-data.ts b/ui-ngx/src/app/shared/models/page/page-data.ts new file mode 100644 index 0000000000..76d1438d4e --- /dev/null +++ b/ui-ngx/src/app/shared/models/page/page-data.ts @@ -0,0 +1,33 @@ +/// +/// Copyright © 2016-2019 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 { BaseData, HasId } from '@shared/models/base-data'; + +export interface PageData> { + data: Array; + totalPages: number; + totalElements: number; + hasNext: boolean; +} + +export function emptyPageData>(): PageData { + return { + data: [], + totalPages: 0, + totalElements: 0, + hasNext: false + } as PageData; +} diff --git a/ui-ngx/src/app/shared/models/page/page-link.ts b/ui-ngx/src/app/shared/models/page/page-link.ts new file mode 100644 index 0000000000..e2cf956b2b --- /dev/null +++ b/ui-ngx/src/app/shared/models/page/page-link.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2019 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 { Direction, SortOrder } from '@shared/models/page/sort-order'; + +export class PageLink { + + textSearch: string; + pageSize: number; + page: number; + sortOrder: SortOrder; + + constructor(pageSize: number, page: number = 0, textSearch: string = null, sortOrder: SortOrder = null) { + this.textSearch = textSearch; + this.pageSize = pageSize; + this.page = page; + this.sortOrder = sortOrder; + } + + public nextPageLink(): PageLink { + return new PageLink(this.pageSize, this.page + 1, this.textSearch, this.sortOrder); + } + + public toQuery(): string { + let query = `?pageSize=${this.pageSize}&page=${this.page}`; + if (this.textSearch && this.textSearch.length) { + query += `&textSearch=${this.textSearch}`; + } + if (this.sortOrder) { + query += `&sortProperty=${this.sortOrder.property}&sortOrder=${this.sortOrder.direction}`; + } + return query; + } + + public sort(item1: any, item2: any): number { + if (this.sortOrder) { + const property = this.sortOrder.property; + const item1Value = item1[property]; + const item2Value = item2[property]; + let result = 0; + if (item1Value !== item2Value) { + if (typeof item1Value === 'number' && typeof item2Value === 'number') { + result = item1Value - item2Value; + } else if (typeof item1Value === 'string' && typeof item2Value === 'string') { + result = item1Value.localeCompare(item2Value); + } else if (typeof item1Value !== typeof item2Value) { + result = 1; + } + } + return this.sortOrder.direction === Direction.ASC ? result : result * -1; + } + return 0; + } + +} + +export class TimePageLink extends PageLink { + + startTime: number; + endTime: number; + + constructor(pageSize: number, page: number = 0, textSearch: string = null, sortOrder: SortOrder = null, + startTime: number = null, endTime: number = null) { + super(pageSize, page, textSearch, sortOrder); + this.startTime = startTime; + this.endTime = endTime; + } + + public nextPageLink(): TimePageLink { + return new TimePageLink(this.pageSize, this.page + 1, this.textSearch, this.sortOrder, this.startTime, this.endTime); + } + + public toQuery(): string { + let query = super.toQuery(); + if (this.startTime) { + query += `&startTime=${this.startTime}`; + } + if (this.endTime) { + query += `&endTime=${this.endTime}`; + } + return query; + } +} diff --git a/ui-ngx/src/app/shared/models/page/sort-order.ts b/ui-ngx/src/app/shared/models/page/sort-order.ts new file mode 100644 index 0000000000..b51791ca7d --- /dev/null +++ b/ui-ngx/src/app/shared/models/page/sort-order.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2019 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. +/// + + +export interface SortOrder { + property: string; + direction: Direction; +} + +export enum Direction { + ASC = 'ASC', + DESC = 'DESC' +} diff --git a/ui-ngx/src/app/shared/models/settings.models.ts b/ui-ngx/src/app/shared/models/settings.models.ts new file mode 100644 index 0000000000..6af535d257 --- /dev/null +++ b/ui-ngx/src/app/shared/models/settings.models.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2019 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. +/// + +export const smtpPortPattern: RegExp = /^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/; + +export interface AdminSettings { + key: string; + jsonValue: T; +} + +export declare type SmtpProtocol = 'smtp' | 'smtps'; + +export interface MailServerSettings { + mailFrom: string; + smtpProtocol: SmtpProtocol; + smtpHost: string; + smtpPort: number; + timeout: number; + enableTls: boolean; + username: string; + password: string; +} diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts new file mode 100644 index 0000000000..060103b5af --- /dev/null +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -0,0 +1,25 @@ +/// +/// Copyright © 2016-2019 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 { CustomerId } from '@shared/models/id/customer-id'; +import { ContactBased } from '@shared/models/contact-based.model'; +import {TenantId} from './id/tenant-id'; + +export interface Tenant extends ContactBased { + title: string; + region: string; + additionalInfo?: any; +} diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts new file mode 100644 index 0000000000..4958082d4f --- /dev/null +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -0,0 +1,315 @@ +/// +/// Copyright © 2016-2019 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 { TimeService } from '@core/services/time.service'; + +export const SECOND = 1000; +export const MINUTE = 60 * SECOND; +export const HOUR = 60 * MINUTE; +export const DAY = 24 * HOUR; + +export enum TimewindowType { + REALTIME, + HISTORY +} + +export enum HistoryWindowType { + LAST_INTERVAL, + FIXED +} + +export class Timewindow { + + displayValue?: string; + selectedTab?: TimewindowType; + realtime?: IntervalWindow; + history?: HistoryWindow; + aggregation?: Aggregation; + + public static historyInterval(timewindowMs: number): Timewindow { + const timewindow = new Timewindow(); + timewindow.history = new HistoryWindow(); + timewindow.history.timewindowMs = timewindowMs; + return timewindow; + } + + public static defaultTimewindow(timeService: TimeService): Timewindow { + const currentTime = new Date().getTime(); + const timewindow = new Timewindow(); + timewindow.displayValue = ''; + timewindow.selectedTab = TimewindowType.REALTIME; + timewindow.realtime = new IntervalWindow(); + timewindow.realtime.interval = SECOND; + timewindow.realtime.timewindowMs = MINUTE; + timewindow.history = new HistoryWindow(); + timewindow.history.historyType = HistoryWindowType.LAST_INTERVAL; + timewindow.history.interval = SECOND; + timewindow.history.timewindowMs = MINUTE; + timewindow.history.fixedTimewindow = new FixedWindow(); + timewindow.history.fixedTimewindow.startTimeMs = currentTime - DAY; + timewindow.history.fixedTimewindow.endTimeMs = currentTime; + timewindow.aggregation = new Aggregation(); + timewindow.aggregation.type = AggregationType.AVG; + timewindow.aggregation.limit = Math.floor(timeService.getMaxDatapointsLimit() / 2); + return timewindow; + } + + public static initModelFromDefaultTimewindow(value: Timewindow, timeService: TimeService): Timewindow { + const model = Timewindow.defaultTimewindow(timeService); + if (value) { + if (value.realtime) { + model.selectedTab = TimewindowType.REALTIME; + if (typeof value.realtime.interval !== 'undefined') { + model.realtime.interval = value.realtime.interval; + } + model.realtime.timewindowMs = value.realtime.timewindowMs; + } else { + model.selectedTab = TimewindowType.HISTORY; + if (typeof value.history.interval !== 'undefined') { + model.history.interval = value.history.interval; + } + if (typeof value.history.timewindowMs !== 'undefined') { + model.history.historyType = HistoryWindowType.LAST_INTERVAL; + model.history.timewindowMs = value.history.timewindowMs; + } else { + model.history.historyType = HistoryWindowType.FIXED; + model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs; + model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs; + } + } + if (value.aggregation) { + if (value.aggregation.type) { + model.aggregation.type = value.aggregation.type; + } + model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2); + } + } + return model; + } + + public clone(): Timewindow { + const cloned = new Timewindow(); + cloned.displayValue = this.displayValue; + cloned.selectedTab = this.selectedTab; + cloned.realtime = this.realtime ? this.realtime.clone() : null; + cloned.history = this.history ? this.history.clone() : null; + cloned.aggregation = this.aggregation ? this.aggregation.clone() : null; + return cloned; + } + + public cloneSelectedTimewindow(): Timewindow { + const cloned = new Timewindow(); + if (typeof this.selectedTab !== 'undefined') { + if (this.selectedTab === TimewindowType.REALTIME) { + cloned.realtime = this.realtime ? this.realtime.clone() : null; + } else if (this.selectedTab === TimewindowType.HISTORY) { + cloned.history = this.history ? this.history.cloneSelectedTimewindow() : null; + } + } + cloned.aggregation = this.aggregation ? this.aggregation.clone() : null; + return cloned; + } + +} + +export class IntervalWindow { + interval?: number; + timewindowMs?: number; + + public clone(): IntervalWindow { + const cloned = new IntervalWindow(); + cloned.interval = this.interval; + cloned.timewindowMs = this.timewindowMs; + return cloned; + } +} + +export class FixedWindow { + startTimeMs: number; + endTimeMs: number; + + public clone(): FixedWindow { + const cloned = new FixedWindow(); + cloned.startTimeMs = this.startTimeMs; + cloned.endTimeMs = this.endTimeMs; + return cloned; + } +} + +export class HistoryWindow extends IntervalWindow { + historyType?: HistoryWindowType; + fixedTimewindow?: FixedWindow; + + public clone(): HistoryWindow { + const cloned = new HistoryWindow(); + cloned.historyType = this.historyType; + if (this.fixedTimewindow) { + cloned.fixedTimewindow = this.fixedTimewindow.clone(); + } + cloned.interval = this.interval; + cloned.timewindowMs = this.timewindowMs; + return cloned; + } + + public cloneSelectedTimewindow(): HistoryWindow { + const cloned = new HistoryWindow(); + if (typeof this.historyType !== 'undefined') { + cloned.interval = this.interval; + if (this.historyType === HistoryWindowType.LAST_INTERVAL) { + cloned.timewindowMs = this.timewindowMs; + } else if (this.historyType === HistoryWindowType.FIXED) { + cloned.fixedTimewindow = this.fixedTimewindow ? this.fixedTimewindow.clone() : null; + } + } + return cloned; + } +} + +export class Aggregation { + type: AggregationType; + limit: number; + + public clone(): Aggregation { + const cloned = new Aggregation(); + cloned.type = this.type; + cloned.limit = this.limit; + return cloned; + } +} + +export enum AggregationType { + MIN = 'MIN', + MAX = 'MAX', + AVG = 'AVG', + SUM = 'SUM', + COUNT = 'COUNT', + NONE = 'NONE' +} + +export const aggregationTranslations = new Map( + [ + [AggregationType.MIN, 'aggregation.min'], + [AggregationType.MAX, 'aggregation.max'], + [AggregationType.AVG, 'aggregation.avg'], + [AggregationType.SUM, 'aggregation.sum'], + [AggregationType.COUNT, 'aggregation.count'], + [AggregationType.NONE, 'aggregation.none'], + ] +); + +export interface TimeInterval { + name: string; + translateParams: {[key: string]: any}; + value: number; +} + +export const defaultTimeIntervals = new Array( + { + name: 'timeinterval.seconds-interval', + translateParams: {seconds: 1}, + value: 1 * SECOND + }, + { + name: 'timeinterval.seconds-interval', + translateParams: {seconds: 5}, + value: 5 * SECOND + }, + { + name: 'timeinterval.seconds-interval', + translateParams: {seconds: 10}, + value: 10 * SECOND + }, + { + name: 'timeinterval.seconds-interval', + translateParams: {seconds: 15}, + value: 15 * SECOND + }, + { + name: 'timeinterval.seconds-interval', + translateParams: {seconds: 30}, + value: 30 * SECOND + }, + { + name: 'timeinterval.minutes-interval', + translateParams: {minutes: 1}, + value: 1 * MINUTE + }, + { + name: 'timeinterval.minutes-interval', + translateParams: {minutes: 2}, + value: 2 * MINUTE + }, + { + name: 'timeinterval.minutes-interval', + translateParams: {minutes: 5}, + value: 5 * MINUTE + }, + { + name: 'timeinterval.minutes-interval', + translateParams: {minutes: 10}, + value: 10 * MINUTE + }, + { + name: 'timeinterval.minutes-interval', + translateParams: {minutes: 15}, + value: 15 * MINUTE + }, + { + name: 'timeinterval.minutes-interval', + translateParams: {minutes: 30}, + value: 30 * MINUTE + }, + { + name: 'timeinterval.hours-interval', + translateParams: {hours: 1}, + value: 1 * HOUR + }, + { + name: 'timeinterval.hours-interval', + translateParams: {hours: 2}, + value: 2 * HOUR + }, + { + name: 'timeinterval.hours-interval', + translateParams: {hours: 5}, + value: 5 * HOUR + }, + { + name: 'timeinterval.hours-interval', + translateParams: {hours: 10}, + value: 10 * HOUR + }, + { + name: 'timeinterval.hours-interval', + translateParams: {hours: 12}, + value: 12 * HOUR + }, + { + name: 'timeinterval.days-interval', + translateParams: {days: 1}, + value: 1 * DAY + }, + { + name: 'timeinterval.days-interval', + translateParams: {days: 7}, + value: 7 * DAY + }, + { + name: 'timeinterval.days-interval', + translateParams: {days: 30}, + value: 30 * DAY + } +); diff --git a/ui-ngx/src/app/shared/models/user.model.ts b/ui-ngx/src/app/shared/models/user.model.ts new file mode 100644 index 0000000000..86ed38a416 --- /dev/null +++ b/ui-ngx/src/app/shared/models/user.model.ts @@ -0,0 +1,56 @@ +/// +/// Copyright © 2016-2019 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 { BaseData } from './base-data'; +import { UserId } from './id/user-id'; +import { CustomerId } from './id/customer-id'; +import { Authority } from './authority.enum'; +import {TenantId} from './id/tenant-id'; + +export interface User extends BaseData { + tenantId: TenantId; + customerId: CustomerId; + email: string; + authority: Authority; + firstName: string; + lastName: string; + additionalInfo: any; +} + +export enum ActivationMethod { + DISPLAY_ACTIVATION_LINK, + SEND_ACTIVATION_MAIL +} + +export const activationMethodTranslations = new Map( + [ + [ActivationMethod.DISPLAY_ACTIVATION_LINK, 'user.display-activation-link'], + [ActivationMethod.SEND_ACTIVATION_MAIL, 'user.send-activation-mail'] + ] +); + +export interface AuthUser { + sub: string; + scopes: string[]; + userId: string; + firstName: string; + lastName: string; + enabled: boolean; + tenantId: string; + customerId: string; + isPublic: boolean; + authority: Authority; +} diff --git a/ui-ngx/src/app/shared/pipe/nospace.pipe.ts b/ui-ngx/src/app/shared/pipe/nospace.pipe.ts new file mode 100644 index 0000000000..62d049947f --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/nospace.pipe.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2019 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 { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'nospace' +}) +export class NospacePipe implements PipeTransform { + + transform(value: string, args?: any): string { + return (!value) ? '' : value.replace(/ /g, ''); + } + +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts new file mode 100644 index 0000000000..0106a0422c --- /dev/null +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -0,0 +1,224 @@ +/// +/// Copyright © 2016-2019 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 { NgModule } from '@angular/core'; +import { CommonModule, DatePipe } from '@angular/common'; +import { FooterComponent } from './components/footer.component'; +import { LogoComponent } from './components/logo.component'; +import { ToastDirective, TbSnackBarComponent } from './components/toast.directive'; +import { BreadcrumbComponent } from '@app/shared/components/breadcrumb.component'; + +import { + MatButtonModule, + MatCheckboxModule, + MatIconModule, + MatCardModule, + MatProgressBarModule, + MatInputModule, + MatSnackBarModule, + MatSidenavModule, + MatToolbarModule, + MatMenuModule, + MatGridListModule, + MatDialogModule, + MatSelectModule, + MatTooltipModule, + MatTableModule, + MatPaginatorModule, + MatSortModule, + MatProgressSpinnerModule, + MatDividerModule, + MatTabsModule, + MatRadioModule, + MatSlideToggleModule, + MatDatepickerModule, + MatSliderModule, + MatExpansionModule, + MatStepperModule, MatAutocompleteModule +} from '@angular/material'; +import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { UserMenuComponent } from '@shared/components/user-menu.component'; +import { NospacePipe } from './pipe/nospace.pipe'; +import { TranslateModule } from '@ngx-translate/core'; +import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component'; +import { HelpComponent } from '@shared/components/help.component'; +// import { EntitiesTableComponent } from '@shared/components/entity/entities-table.component'; +// import { AddEntityDialogComponent } from '@shared/components/entity/add-entity-dialog.component'; +// import { DetailsPanelComponent } from '@shared/components/details-panel.component'; +// import { EntityDetailsPanelComponent } from '@shared/components/entity/entity-details-panel.component'; +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; +// import { ContactComponent } from '@shared/components/contact.component'; +// import { AuditLogDetailsDialogComponent } from '@shared/components/audit-log/audit-log-details-dialog.component'; +// import { AuditLogTableComponent } from '@shared/components/audit-log/audit-log-table.component'; +// import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; +// import { TimewindowComponent } from '@shared/components/time/timewindow.component'; +import { OverlayModule } from '@angular/cdk/overlay'; +// import { TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component'; +// import { TimeintervalComponent } from '@shared/components/time/timeinterval.component'; +// import { DatetimePeriodComponent } from '@shared/components/time/datetime-period.component'; +// import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe'; +import { ClipboardModule } from 'ngx-clipboard'; +// import { ValueInputComponent } from '@shared/components/value-input.component'; +// import { IntervalCountPipe } from '@shared/pipe/interval-count.pipe'; +import { FullscreenDirective } from '@shared/components/fullscreen.directive'; + +@NgModule({ + providers: [ + DatePipe, +// MillisecondsToTimeStringPipe, +// EnumToArrayPipe, +// IntervalCountPipe, + ], + entryComponents: [ + TbSnackBarComponent, + TbAnchorComponent, +// AddEntityDialogComponent, +// AuditLogDetailsDialogComponent, +// TimewindowPanelComponent, + ], + declarations: [ + FooterComponent, + LogoComponent, + ToastDirective, + FullscreenDirective, + TbAnchorComponent, + HelpComponent, + TbCheckboxComponent, + TbSnackBarComponent, + BreadcrumbComponent, + UserMenuComponent, +// EntitiesTableComponent, +// AddEntityDialogComponent, +// DetailsPanelComponent, +// EntityDetailsPanelComponent, +// ContactComponent, +// AuditLogTableComponent, +// AuditLogDetailsDialogComponent, +// TimewindowComponent, +// TimewindowPanelComponent, +// TimeintervalComponent, +// DatetimePeriodComponent, +// ValueInputComponent, + NospacePipe, +// MillisecondsToTimeStringPipe, +// EnumToArrayPipe, +// IntervalCountPipe + ], + imports: [ + CommonModule, + RouterModule, + TranslateModule, + MatButtonModule, + MatCheckboxModule, + MatIconModule, + MatCardModule, + MatProgressBarModule, + MatInputModule, + MatSnackBarModule, + MatSidenavModule, + MatToolbarModule, + MatMenuModule, + MatGridListModule, + MatDialogModule, + MatSelectModule, + MatTooltipModule, + MatTableModule, + MatPaginatorModule, + MatSortModule, + MatProgressSpinnerModule, + MatDividerModule, + MatTabsModule, + MatRadioModule, + MatSlideToggleModule, + MatDatepickerModule, + MatNativeDatetimeModule, + MatDatetimepickerModule, + MatSliderModule, + MatExpansionModule, + MatStepperModule, + MatAutocompleteModule, + ClipboardModule, + FlexLayoutModule.withConfig({addFlexToParent: false}), + FormsModule, + ReactiveFormsModule, + OverlayModule + ], + exports: [ + FooterComponent, + LogoComponent, + ToastDirective, + FullscreenDirective, + TbAnchorComponent, + HelpComponent, + TbCheckboxComponent, + BreadcrumbComponent, + UserMenuComponent, +// EntitiesTableComponent, +// AddEntityDialogComponent, +// DetailsPanelComponent, +// EntityDetailsPanelComponent, +// ContactComponent, +// AuditLogTableComponent, +// TimewindowComponent, +// TimewindowPanelComponent, +// TimeintervalComponent, +// DatetimePeriodComponent, +// ValueInputComponent, + MatButtonModule, + MatCheckboxModule, + MatIconModule, + MatCardModule, + MatProgressBarModule, + MatInputModule, + MatSnackBarModule, + MatSidenavModule, + MatToolbarModule, + MatMenuModule, + MatGridListModule, + MatDialogModule, + MatSelectModule, + MatTooltipModule, + MatTableModule, + MatPaginatorModule, + MatSortModule, + MatProgressSpinnerModule, + MatDividerModule, + MatTabsModule, + MatRadioModule, + MatSlideToggleModule, + MatDatepickerModule, + MatNativeDatetimeModule, + MatDatetimepickerModule, + MatSliderModule, + MatExpansionModule, + MatStepperModule, + MatAutocompleteModule, + ClipboardModule, + FlexLayoutModule, + FormsModule, + ReactiveFormsModule, + OverlayModule, + NospacePipe, +// MillisecondsToTimeStringPipe, +// EnumToArrayPipe, +// IntervalCountPipe, + TranslateModule + ] +}) +export class SharedModule { } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 00b3ca21e0..88884094c8 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -407,7 +407,9 @@ "customer-required": "Customer is required", "select-default-customer": "Select default customer", "default-customer": "Default customer", - "default-customer-required": "Default customer is required in order to debug dashboard on Tenant level" + "default-customer-required": "Default customer is required in order to debug dashboard on Tenant level", + "search": "Search customers", + "selected-customers": "{ count, plural, 1 {1 customer} other {# customers} } selected" }, "datetime": { "date-from": "Date from", @@ -1385,7 +1387,9 @@ "idCopiedMessage": "Tenant Id has been copied to clipboard", "select-tenant": "Select tenant", "no-tenants-matching": "No tenants matching '{{entity}}' were found.", - "tenant-required": "Tenant is required" + "tenant-required": "Tenant is required", + "search": "Search tenants", + "selected-tenants": "{ count, plural, 1 {1 tenant} other {# tenants} } selected" }, "timeinterval": { "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", @@ -1453,7 +1457,9 @@ "activation-link-copied-message": "User activation link has been copied to clipboard", "details": "Details", "login-as-tenant-admin": "Login as Tenant Admin", - "login-as-customer-user": "Login as Customer User" + "login-as-customer-user": "Login as Customer User", + "search": "Search users", + "selected-users": "{ count, plural, 1 {1 user} other {# users} } selected" }, "value": { "type": "Value type", diff --git a/ui-ngx/src/theme.scss b/ui-ngx/src/theme.scss index d0e4c17146..33b7b11673 100644 --- a/ui-ngx/src/theme.scss +++ b/ui-ngx/src/theme.scss @@ -107,7 +107,7 @@ $tb-dark-mat-indigo: ( 500: $tb-dark-primary-color, 600: $tb-secondary-color, 700: #303f9f, - 800: #283593, + 800: $tb-primary-color, 900: #1a237e, A100: $tb-hue3-color, A200: #536dfe, @@ -135,18 +135,18 @@ $tb-dark-primary: mat-palette($tb-dark-mat-indigo); $tb-dark-theme-background: ( status-bar: black, - app-bar: map_get($tb-mat-indigo, 900), - background: map_get($tb-mat-indigo, 800), + app-bar: map_get($tb-dark-mat-indigo, 900), + background: map_get($tb-dark-mat-indigo, 800), hover: rgba(white, 0.04), - card: map_get($tb-mat-indigo, 800), - dialog: map_get($tb-mat-indigo, 800), + card: map_get($tb-dark-mat-indigo, 800), + dialog: map_get($tb-dark-mat-indigo, 800), disabled-button: rgba(white, 0.12), - raised-button: map-get($tb-mat-indigo, 50), + raised-button: map-get($tb-dark-mat-indigo, 50), focused-button: $light-focused, - selected-button: map_get($tb-mat-indigo, 900), - selected-disabled-button: map_get($tb-mat-indigo, 800), + selected-button: map_get($tb-dark-mat-indigo, 900), + selected-disabled-button: map_get($tb-dark-mat-indigo, 800), disabled-button-toggle: black, - unselected-chip: map_get($tb-mat-indigo, 700), + unselected-chip: map_get($tb-dark-mat-indigo, 700), disabled-list-option: black, );