Commit 35e34bf2 authored by ibuler's avatar ibuler

[Update] 优化搜索

parent f0abd8f7
{
"reset": "重置",
"submit": "提交",
"email subject prefix": "邮件主题前缀",
"basic setting": "基本设置",
"email setting": "邮件设置",
"ldap setting": "LDAP设置",
"terminal setting": "终端设置",
"current site url": "当前站点URL",
"user guide url": "用户向导URL",
"user first login update profile done redirect to it": "用户第一次登录,修改profile后重定向到地址",
"server": "服务器",
"view": "视图",
"help": "帮助",
"hide left manager": "隐藏左边栏",
"show left manager": "显示左边栏",
"disconnect all": "断开所有链接",
"disconnect": "断开链接",
"website": "官网",
"search": "搜索",
"settings": "系统设置",
"job center": "作业中心",
"sessions": "会话管理",
"perms": "权限管理",
"assets": "资产管理",
"users": "用户管理",
"dashboard": "仪表盘",
"task": "任务",
"session online": "在线会话",
"session offline": "离线会话",
"commands": "命令记录",
"terminal": "终端管理",
"asset perminssion": "资产授权",
"asset": "资产",
"asset group": "资产组",
"cluster": "集群",
"admin user": "管理用户",
"system user": "系统用户",
"labels": "标签管理",
"user": "用户",
"user group": "用户组",
"login logs": "登陆日志",
"language": "语言选择",
"found": "发现",
"users ": "用户",
"choose a user": "选择一个用户",
"please choose a user": "请选择一个用户",
"cancel": "取消",
"confirm": "确认",
"document": "文档",
"support": "商业支持",
"speed": "速度",
"file manager": "文件管理",
"file": "文件管理",
"new connection": "连接",
"connect": "连接",
"rdp resolution": "RDP分辨率",
"set rdp solution": "设置分辨率",
"select a solution": "选择分辨率",
"set font": "设置字体",
"font": "字体",
"font size": "字体大小",
"full screen": "全屏显示",
"please input password": "请输入密码",
"username": "用户名",
"password": "密码",
"skip": "跳过",
"loading": "加载中"
}
{
"reset": "重置",
"submit": "提交",
"email subject prefix": "邮件主题前缀",
"basic setting": "基本设置",
"email setting": "邮件设置",
"ldap setting": "LDAP设置",
"terminal setting": "终端设置",
"current site url": "当前站点URL",
"user guide url": "用户向导URL",
"user first login update profile done redirect to it": "用户第一次登录,修改profile后重定向到地址",
"server": "服务器",
"view": "视图",
"help": "帮助",
"hide left manager": "隐藏左边栏",
"show left manager": "显示左边栏",
"disconnect all": "断开所有链接",
"disconnect": "断开链接",
"website": "官网",
"search": "搜索",
"settings": "系统设置",
"job center": "作业中心",
"sessions": "会话管理",
"perms": "权限管理",
"assets": "资产管理",
"users": "用户管理",
"dashboard": "仪表盘",
"task": "任务",
"session online": "在线会话",
"session offline": "离线会话",
"commands": "命令记录",
"terminal": "终端管理",
"asset perminssion": "资产授权",
"asset": "资产",
"asset group": "资产组",
"cluster": "集群",
"admin user": "管理用户",
"system user": "系统用户",
"labels": "标签管理",
"user": "用户",
"user group": "用户组",
"login logs": "登陆日志",
"language": "语言选择",
"found": "发现",
"users ": "用户",
"choose a user": "选择一个用户",
"please choose a user": "请选择一个用户",
"cancel": "取消",
"confirm": "确认",
"document": "文档",
"support": "商业支持",
"speed": "速度",
"file manager": "文件管理",
"file": "文件管理",
"new connection": "连接",
"connect": "连接",
"rdp resolution": "RDP分辨率",
"set rdp solution": "设置分辨率",
"select a solution": "选择分辨率",
"set font": "设置字体",
"font": "字体",
"font size": "字体大小",
"full screen": "全屏显示",
"please input password": "请输入密码",
"username": "用户名",
"password": "密码",
"skip": "跳过",
"loading": "加载中"
}
...@@ -13,17 +13,14 @@ import {HttpClientModule} from '@angular/common/http'; ...@@ -13,17 +13,14 @@ import {HttpClientModule} from '@angular/common/http';
import {AppRouterModule} from './router/router.module'; import {AppRouterModule} from './router/router.module';
import {AppComponent} from './pages/app.component';
// service // service
import {AppService, HttpService, LocalStorageService, NavService, LogService, UUIDService} from './app.service'; import {AppService, HttpService, LocalStorageService, NavService, LogService, UUIDService, TreeFilterService} from './app.service';
import {CookieService} from 'ngx-cookie-service'; import {CookieService} from 'ngx-cookie-service';
import {MAT_LABEL_GLOBAL_OPTIONS} from '@angular/material'; import {MAT_LABEL_GLOBAL_OPTIONS} from '@angular/material';
import {Pipes} from './pipes/pipes'; import {Pipes} from './pipes/pipes';
import {AppComponent} from './pages/app.component';
import {PagesComponents} from './pages/pages.component'; import {PagesComponents} from './pages/pages.component';
import {ElementComponents} from './elements/elements.component'; import {ElementComponents} from './elements/elements.component';
import {ChangLanWarningDialogComponent, RDPSolutionDialogComponent, FontDialogComponent} from './elements/nav/nav.component'; import {ChangLanWarningDialogComponent, RDPSolutionDialogComponent, FontDialogComponent} from './elements/nav/nav.component';
...@@ -71,9 +68,9 @@ import {SftpComponent} from './elements/sftp/sftp.component'; ...@@ -71,9 +68,9 @@ import {SftpComponent} from './elements/sftp/sftp.component';
LocalStorageService, LocalStorageService,
DialogService, DialogService,
CookieService, CookieService,
TreeFilterService,
NGXLogger, NGXLogger,
{provide: MAT_LABEL_GLOBAL_OPTIONS, useValue: {float: 'always'}} {provide: MAT_LABEL_GLOBAL_OPTIONS, useValue: {float: 'always'}}
] ]
}) })
export class AppModule { export class AppModule {
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
* @date 2017-11-07 * @date 2017-11-07
* @author liuzheng <liuzheng712@gmail.com> * @author liuzheng <liuzheng712@gmail.com>
*/ */
import {Injectable, OnInit} from '@angular/core'; import {EventEmitter, Injectable, OnInit} from '@angular/core';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {CookieService} from 'ngx-cookie-service'; import {CookieService} from 'ngx-cookie-service';
import 'rxjs/add/operator/map'; import 'rxjs/add/operator/map';
...@@ -13,7 +13,8 @@ import 'rxjs/add/operator/catch'; ...@@ -13,7 +13,8 @@ import 'rxjs/add/operator/catch';
import {DataStore, User, Browser, i18n} from './globals'; import {DataStore, User, Browser, i18n} from './globals';
import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'; import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http';
import {NGXLogger} from 'ngx-logger'; import {NGXLogger} from 'ngx-logger';
import {SystemUser, GuacObjAddResp, TreeNode} from './model'; import {SystemUser, GuacObjAddResp, TreeNode, User as _User} from './model';
import {environment} from '../environments/environment';
import * as UUID from 'uuid-js/lib/uuid.js'; import * as UUID from 'uuid-js/lib/uuid.js';
declare function unescape(s: string): string; declare function unescape(s: string): string;
...@@ -64,7 +65,7 @@ export class HttpService { ...@@ -64,7 +65,7 @@ export class HttpService {
} }
getUserProfile() { getUserProfile() {
return this.http.get('/api/users/v1/profile/'); return this.http.get<_User>('/api/users/v1/profile/');
} }
getMyGrantedNodes(async: boolean, refresh?: boolean) { getMyGrantedNodes(async: boolean, refresh?: boolean) {
...@@ -111,7 +112,7 @@ export class HttpService { ...@@ -111,7 +112,7 @@ export class HttpService {
.set('user_id', userId) .set('user_id', userId)
.set('asset_id', assetId) .set('asset_id', assetId)
.set('system_user_id', systemUserId) .set('system_user_id', systemUserId)
.set('token', DataStore.guacamole_token); .set('token', DataStore.guacamoleToken);
let body = new HttpParams(); let body = new HttpParams();
if (systemUserUsername && systemUserPassword) { if (systemUserUsername && systemUserPassword) {
systemUserUsername = btoa(systemUserUsername); systemUserUsername = btoa(systemUserUsername);
...@@ -139,7 +140,7 @@ export class HttpService { ...@@ -139,7 +140,7 @@ export class HttpService {
let params = new HttpParams() let params = new HttpParams()
.set('user_id', userId) .set('user_id', userId)
.set('remote_app_id', remoteAppId) .set('remote_app_id', remoteAppId)
.set('token', DataStore.guacamole_token); .set('token', DataStore.guacamoleToken);
let body = new HttpParams(); let body = new HttpParams();
if (systemUserUsername && systemUserPassword) { if (systemUserUsername && systemUserPassword) {
systemUserUsername = btoa(systemUserUsername); systemUserUsername = btoa(systemUserUsername);
...@@ -280,46 +281,41 @@ export class AppService implements OnInit { ...@@ -280,46 +281,41 @@ export class AppService implements OnInit {
constructor(private _http: HttpService, constructor(private _http: HttpService,
private _router: Router, private _router: Router,
private _logger: LogService,
private _cookie: CookieService, private _cookie: CookieService,
private _logger: LogService,
private _localStorage: LocalStorageService) { private _localStorage: LocalStorageService) {
if (this._cookie.get('loglevel')) { this.setLogLevel();
// 0.- Level.OFF this.setLang();
// 1.- Level.ERROR this.checklogin();
// 2.- Level.WARN
// 3.- Level.INFO
// 4.- Level.DEBUG
// 5.- Level.LOG
this._logger.level = parseInt(this._cookie.get('loglevel'), 10);
// this._logger.debug('Your debug stuff');
// this._logger.info('An info');
// this._logger.warn('Take care ');
// this._logger.error('Too late !');
// this._logger.log('log !');
} else {
this._cookie.set('loglevel', '0', 99, '/', document.domain);
// this._logger.level = parseInt(Cookie.getCookie('loglevel'));
this._logger.level = 0;
} }
// if (environment.production) { ngOnInit() {
this._logger.level = 2; }
this.checklogin();
// }
if (this._cookie.get('lang')) { setLogLevel() {
this.lang = this._cookie.get('lang'); // 设置logger level
} else { let logLevel = this._cookie.get('logLevel');
this.lang = window.navigator.languages ? window.navigator.languages[0] : 'cn'; if (!logLevel) {
this._cookie.set('lang', this.lang); logLevel = environment.production ? '1' : '5';
}
this._logger.level = parseInt(logLevel, 10);
} }
if (this.lang !== 'en') { setLang() {
this._http.get('/luna/i18n/' + this.lang + '.json').subscribe( let lang = this._cookie.get('lang');
if (!lang) {
lang = navigator.language;
}
lang = lang.substr(0, 2);
this.lang = lang;
if (lang !== 'en') {
this._http.get('/luna/i18n/' + 'zh' + '.json').subscribe(
data => { data => {
this._localStorage.set('lang', JSON.stringify(data)); this._localStorage.set('lang', JSON.stringify(data));
}, },
err => { err => {
this._logger.error('Load i18n file error: ', err.error);
} }
); );
} }
...@@ -332,42 +328,26 @@ export class AppService implements OnInit { ...@@ -332,42 +328,26 @@ export class AppService implements OnInit {
} }
} }
ngOnInit() {
}
checklogin() { checklogin() {
this._logger.log('service.ts:AppService,checklogin'); this._logger.log('service.ts:AppService,checklogin');
if (DataStore.Path) { if (DataStore.Path) {
if (document.location.pathname === '/luna/connect') { if (document.location.pathname === '/luna/connect') {
} else { return;
}
if (User.logined) { if (User.logined) {
if (document.location.pathname === '/login') { if (document.location.pathname === '/login') {
this._router.navigate(['']); this._router.navigate(['']);
} else { } else {
this._router.navigate([document.location.pathname]); this._router.navigate([document.location.pathname]);
} }
return;
// jQuery('angular2').show(); // jQuery('angular2').show();
} else { }
this._http.getUserProfile() this._http.getUserProfile().subscribe(
.subscribe( user => {
data => { Object.assign(User, user);
User.id = data['id']; User.logined = true;
User.name = data['name']; this._localStorage.set('user', user.id);
User.username = data['username'];
User.email = data['email'];
User.is_active = data['is_active'];
User.is_superuser = data['is_superuser'];
User.role = data['role'];
// User.groups = data['groups'];
User.wechat = data['wechat'];
User.comment = data['comment'];
User.date_expired = data['date_expired'];
if (data['phone']) {
User.phone = data['phone'].toString();
}
User.logined = data['logined'];
this._logger.debug(User);
this._localStorage.set('user', data['id']);
}, },
err => { err => {
// this._logger.error(err); // this._logger.error(err);
...@@ -377,8 +357,6 @@ export class AppService implements OnInit { ...@@ -377,8 +357,6 @@ export class AppService implements OnInit {
// this._router.navigate(['login']); // this._router.navigate(['login']);
}, },
); );
}
}
} else { } else {
this._router.navigate(['FOF']); this._router.navigate(['FOF']);
// jQuery('angular2').show(); // jQuery('angular2').show();
...@@ -435,3 +413,12 @@ export class NavService { ...@@ -435,3 +413,12 @@ export class NavService {
this.store.set('SkipAllManualPassword', value); this.store.set('SkipAllManualPassword', value);
} }
} }
@Injectable()
export class TreeFilterService {
onFilter: EventEmitter<string> = new EventEmitter<string>();
filter(q: string) {
this.onFilter.emit(q);
}
}
import {Component, Input, OnInit, Inject, SimpleChanges, OnChanges, ElementRef, ViewChild} from '@angular/core'; import {Component, Input, OnInit, OnDestroy, ElementRef, ViewChild} from '@angular/core';
import {NavList, View} from '../../pages/control/control/control.component';
import {AppService, HttpService, LogService, NavService} from '../../app.service';
import {connectEvt} from '../../globals';
import {MatDialog} from '@angular/material'; import {MatDialog} from '@angular/material';
import {BehaviorSubject} from 'rxjs/BehaviorSubject'; import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {ActivatedRoute} from '@angular/router'; import {ActivatedRoute} from '@angular/router';
import {AppService, HttpService, LogService, NavService, TreeFilterService} from '../../app.service';
import {connectEvt} from '../../globals';
import {TreeNode, ConnectEvt} from '../../model'; import {TreeNode, ConnectEvt} from '../../model';
import * as jQuery from 'jquery/dist/jquery.min'; import {View} from '../content/model';
declare var $: any; declare var $: any;
@Component({ @Component({
selector: 'elements-asset-tree', selector: 'elements-asset-tree',
templateUrl: './asset-tree.component.html', templateUrl: './asset-tree.component.html',
styleUrls: ['./asset-tree.component.scss'] styleUrls: ['./asset-tree.component.scss'],
}) })
export class ElementAssetTreeComponent implements OnInit, OnChanges { export class ElementAssetTreeComponent implements OnInit, OnDestroy {
@Input() query: string; @Input() query: string;
@Input() searchEvt$: BehaviorSubject<string>; @Input() searchEvt$: BehaviorSubject<string>;
@ViewChild('rMenu') rMenu: ElementRef; @ViewChild('rMenu') rMenu: ElementRef;
...@@ -43,47 +43,40 @@ export class ElementAssetTreeComponent implements OnInit, OnChanges { ...@@ -43,47 +43,40 @@ export class ElementAssetTreeComponent implements OnInit, OnChanges {
isShowRMenu = false; isShowRMenu = false;
rightClickSelectNode: any; rightClickSelectNode: any;
hasLoginTo = false; hasLoginTo = false;
loadTreeAsync = false; treeFilterSubscription: any;
onNodeClick(event, treeId, treeNode, clickFlag) {
if (treeNode.isParent) {
this.assetsTree.expandNode(treeNode);
} else {
this._http.getUserProfile().subscribe();
this.Connect(treeNode);
}
}
constructor(private _appSvc: AppService, constructor(private _appSvc: AppService,
private _treeFilterSvc: TreeFilterService,
public _dialog: MatDialog, public _dialog: MatDialog,
public _logger: LogService, public _logger: LogService,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private _http: HttpService, private _http: HttpService,
private _navSvc: NavService private _navSvc: NavService
) { ) {}
this.searchEvt$ = new BehaviorSubject<string>(this.query);
}
ngOnInit() { ngOnInit() {
this.initTree(); this.initTree();
document.addEventListener('click', this.hideRMenu.bind(this), false); document.addEventListener('click', this.hideRMenu.bind(this), false);
this.loadTreeAsync = this._navSvc.treeLoadAsync;
// Todo: 搜索 this.treeFilterSubscription = this._treeFilterSvc.onFilter.subscribe(
this.searchEvt$.asObservable() keyword => {
.debounceTime(300) this._logger.debug('Filter tree: ', keyword);
.distinctUntilChanged() this.filterAssets(keyword);
.subscribe((n) => { this.filterRemoteApps(keyword);
this.filter(); }
}); );
} }
ngOnChanges(changes: SimpleChanges) { ngOnDestroy(): void {
// if (changes['Data'] && this.Data) { this.treeFilterSubscription.unsubscribe();
// this.draw(); }
// }
if (changes['query'] && !changes['query'].firstChange) { onNodeClick(event, treeId, treeNode, clickFlag) {
this.searchEvt$.next(this.query); if (treeNode.isParent) {
this.assetsTree.expandNode(treeNode);
} else {
this._http.getUserProfile().subscribe();
this.ConnectAsset(treeNode);
} }
} }
...@@ -98,7 +91,7 @@ export class ElementAssetTreeComponent implements OnInit, OnChanges { ...@@ -98,7 +91,7 @@ export class ElementAssetTreeComponent implements OnInit, OnChanges {
onClick: this.onNodeClick.bind(this), onClick: this.onNodeClick.bind(this),
onRightClick: this.onRightClick.bind(this) onRightClick: this.onRightClick.bind(this)
}; };
if (this.loadTreeAsync) { if (this._navSvc.treeLoadAsync) {
setting['async'] = { setting['async'] = {
enable: true, enable: true,
url: '/api/perms/v1/users/nodes/children-with-assets/tree/', url: '/api/perms/v1/users/nodes/children-with-assets/tree/',
...@@ -107,7 +100,7 @@ export class ElementAssetTreeComponent implements OnInit, OnChanges { ...@@ -107,7 +100,7 @@ export class ElementAssetTreeComponent implements OnInit, OnChanges {
}; };
} }
this._http.getMyGrantedNodes(this.loadTreeAsync, refresh).subscribe(resp => { this._http.getMyGrantedNodes(this._navSvc.treeLoadAsync, refresh).subscribe(resp => {
const assetsTree = $.fn.zTree.init($('#assetsTree'), setting, resp); const assetsTree = $.fn.zTree.init($('#assetsTree'), setting, resp);
this.assetsTree = assetsTree; this.assetsTree = assetsTree;
this.rootNodeAddDom(assetsTree, () => { this.rootNodeAddDom(assetsTree, () => {
...@@ -144,7 +137,7 @@ export class ElementAssetTreeComponent implements OnInit, OnChanges { ...@@ -144,7 +137,7 @@ export class ElementAssetTreeComponent implements OnInit, OnChanges {
this.Data.forEach(t => { this.Data.forEach(t => {
if (login_to === t.id && t.isParent === false) { if (login_to === t.id && t.isParent === false) {
this.hasLoginTo = true; this.hasLoginTo = true;
this.Connect(t); this.ConnectAsset(t);
return; return;
} }
}); });
...@@ -152,7 +145,7 @@ export class ElementAssetTreeComponent implements OnInit, OnChanges { ...@@ -152,7 +145,7 @@ export class ElementAssetTreeComponent implements OnInit, OnChanges {
}); });
} }
Connect(node: TreeNode) { ConnectAsset(node: TreeNode) {
const evt = new ConnectEvt(node, 'asset'); const evt = new ConnectEvt(node, 'asset');
connectEvt.next(evt); connectEvt.next(evt);
} }
...@@ -215,24 +208,88 @@ export class ElementAssetTreeComponent implements OnInit, OnChanges { ...@@ -215,24 +208,88 @@ export class ElementAssetTreeComponent implements OnInit, OnChanges {
} }
connectFileManager() { connectFileManager() {
const host = this.rightClickSelectNode.meta.asset; const node = this.rightClickSelectNode;
const id = NavList.List.length - 1; const evt = new ConnectEvt(node, 'sftp');
if (host) { connectEvt.next(evt);
NavList.List[id].nick = '[FILE]' + host.hostname;
NavList.List[id].connected = true;
NavList.List[id].edit = false;
NavList.List[id].closed = false;
NavList.List[id].host = host;
NavList.List[id].type = 'sftp';
NavList.List.push(new View());
NavList.Active = id;
jQuery('.tabs').animate({'scrollLeft': 150 * id}, 400);
}
} }
connectTerminal() { connectTerminal() {
const host = this.rightClickSelectNode; const host = this.rightClickSelectNode;
this.Connect(host); this.ConnectAsset(host);
}
filterAssets(keyword) {
if (this._navSvc.treeLoadAsync) {
this._logger.debug('Filter assets server');
this.filterAssetsServer(keyword);
} else {
this._logger.debug('Filter assets local');
this.filterAssetsLocal(keyword);
}
}
filterTree(keyword, tree, filterCallback) {
const nodes = tree.transformToArray(tree.getNodes());
if (!keyword) {
if (tree.hiddenNodes) {
tree.showNodes(tree.hiddenNodes);
tree.hiddenNodes = null;
}
if (tree.expandNodes) {
tree.expandNodes.forEach((node) => {
if (node.id !== nodes[0].id) {
tree.expandNode(node, false);
}
});
tree.expandNodes = null;
}
return null;
}
let shouldShow = [];
const matchedNodes = tree.getNodesByFilter(filterCallback);
matchedNodes.forEach((node) => {
const parents = this.recurseParent(node);
const children = this.recurseChildren(node);
shouldShow = [...shouldShow, ...parents, ...children, node];
});
tree.hiddenNodes = nodes;
tree.expandNodes = shouldShow;
tree.hideNodes(nodes);
tree.showNodes(shouldShow);
shouldShow.forEach((node) => {
if (node.isParent) {
tree.expandNode(node, true);
}
});
}
filterRemoteApps(keyword) {
if (!this.remoteAppsTree) {
return null;
}
function filterCallback(node: TreeNode) {
return node.name.toLowerCase().indexOf(keyword) !== -1;
}
return this.filterTree(keyword, this.remoteAppsTree, filterCallback);
}
filterAssetsServer(keyword) {
return;
}
filterAssetsLocal(keyword) {
if (!this.assetsTree) {
return null;
}
function filterAssetsCallback(node) {
if (node.isParent) {
return false;
}
const host = node.meta.asset;
return host.hostname.toLowerCase().indexOf(keyword) !== -1 || host.ip.indexOf(keyword) !== -1;
}
return this.filterTree(keyword, this.assetsTree, filterAssetsCallback);
// zTreeObj.expandAll(true);
} }
recurseParent(node) { recurseParent(node) {
...@@ -260,54 +317,5 @@ export class ElementAssetTreeComponent implements OnInit, OnChanges { ...@@ -260,54 +317,5 @@ export class ElementAssetTreeComponent implements OnInit, OnChanges {
}); });
return all_children; return all_children;
} }
filter() {
const zTreeObj = $.fn.zTree.getZTreeObj('ztree');
if (!zTreeObj) {
return null;
}
let _keywords = this.query;
const nodes = zTreeObj.transformToArray(zTreeObj.getNodes());
if (!_keywords) {
if (this.hiddenNodes) {
zTreeObj.showNodes(this.hiddenNodes);
this.hiddenNodes = null;
}
if (this.expandNodes) {
this.expandNodes.forEach((node) => {
if (node.id !== nodes[0].id) {
zTreeObj.expandNode(node, false);
}
});
this.expandNodes = null;
}
return null;
}
_keywords = _keywords.toLowerCase();
let shouldShow = [];
const matchedNodes = zTreeObj.getNodesByFilter(function (node) {
if (node.meta.type === 'asset') {
const host = node.meta.asset;
return host.hostname.toLowerCase().indexOf(_keywords) !== -1 || host.ip.indexOf(_keywords) !== -1;
} else {
return node.name.toLowerCase().indexOf(_keywords) !== -1;
}
});
matchedNodes.forEach((node) => {
const parents = this.recurseParent(node);
const children = this.recurseChildren(node);
shouldShow = [...shouldShow, ...parents, ...children, node];
});
this.hiddenNodes = nodes;
this.expandNodes = shouldShow;
zTreeObj.hideNodes(nodes);
zTreeObj.showNodes(shouldShow);
shouldShow.forEach((node) => {
if (node.isParent) {
zTreeObj.expandNode(node, true);
}
});
// zTreeObj.expandAll(true);
}
} }
...@@ -8,15 +8,10 @@ elements-term, elements-guacamole, elements-settings { ...@@ -8,15 +8,10 @@ elements-term, elements-guacamole, elements-settings {
.window { .window {
display: none; display: none;
height: 100%;
/*padding: 15px;*/ /*padding: 15px;*/
} }
.active { .active {
display: block; display: block;
} }
.right-side {
height: 100%;
width: 100%;
background-color: gray;
}
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
> >
</elements-ssh-term> </elements-ssh-term>
<elements-guacamole <elements-guacamole
[view]="view"
[host]="view.host" [host]="view.host"
[sysUser]="view.user" [sysUser]="view.user"
[remoteAppId]="view.remoteApp" [remoteAppId]="view.remoteApp"
......
<div id="content" fxLayout="column" ngxSplit="column" style="width: 100%;height: 100%;"> <div id="content" fxLayout="column" ngxSplit="column" style="width: 100%;height: 100%;">
<div fxFlex="0 0 30px"> <div fxFlex="0 0 30px">
<div class="scroll-botton" style="padding: 0 5px"> <div class="scroll-button" style="padding: 0 5px">
<a class="left" (click)="scrollLeft()"><i class="fa fa-caret-left"></i></a> <a class="left" (click)="scrollLeft()"><i class="fa fa-caret-left"></i></a>
<a class="right" (click)="scrollRight()"><i class="fa fa-caret-right"></i></a> <a class="right" (click)="scrollRight()"><i class="fa fa-caret-right"></i></a>
</div> </div>
...@@ -15,9 +15,7 @@ ...@@ -15,9 +15,7 @@
</div> </div>
</div> </div>
<div fxFlex="0 0 calc(100%-35px)" id="winContainer"> <div fxFlex="0 0 calc(100%-35px)" id="winContainer">
<div class="window" *ngFor="let view of viewList; let i=index"> <elements-content-window *ngFor="let view of viewList" [view]="view" ></elements-content-window>
<elements-content-window [view]="view"></elements-content-window>
</div>
</div> </div>
</div> </div>
......
/*.right-side {*/
/*height: 100%;*/
/*width: 100%;*/
/*background-color: gray;*/
/*}*/
/*div {*/
/*height: 100%;*/
/*width: 100%;*/
/*padding: 0;*/
/*background-color: #1f1b1b;*/
/*margin: 0;*/
/*position: initial;*/
/*}*/
/*#content {*/
/*padding-top: 0;*/
/*}*/
/*.container-fluid {*/
/*padding-top: 30px;*/
/*}*/
.tabs { .tabs {
height: 30px; height: 30px;
overflow-y: hidden; overflow-y: hidden;
...@@ -52,7 +29,7 @@ ...@@ -52,7 +29,7 @@
background-color: #F5F5F5; background-color: #F5F5F5;
} }
.scroll-botton { .scroll-button {
font-size: 20px; font-size: 20px;
float: left; float: left;
height: 30px; height: 30px;
...@@ -60,3 +37,7 @@ ...@@ -60,3 +37,7 @@
background-color: #3a3333; background-color: #3a3333;
color: white color: white
} }
.window {
height: 100%;
}
...@@ -15,7 +15,7 @@ import jQuery from 'jquery/dist/jquery.min'; ...@@ -15,7 +15,7 @@ import jQuery from 'jquery/dist/jquery.min';
@Component({ @Component({
selector: 'elements-content', selector: 'elements-content',
templateUrl: './content.component.html', templateUrl: './content.component.html',
styleUrls: ['./content.component.css'] styleUrls: ['./content.component.scss']
}) })
export class ElementContentComponent implements OnInit { export class ElementContentComponent implements OnInit {
viewList: Array<View> = []; viewList: Array<View> = [];
......
...@@ -3,8 +3,7 @@ import {CookieService} from 'ngx-cookie-service'; ...@@ -3,8 +3,7 @@ import {CookieService} from 'ngx-cookie-service';
import {HttpService, LogService} from '../../app.service'; import {HttpService, LogService} from '../../app.service';
import {DataStore, User} from '../../globals'; import {DataStore, User} from '../../globals';
import {DomSanitizer} from '@angular/platform-browser'; import {DomSanitizer} from '@angular/platform-browser';
import {environment} from '../../../environments/environment'; import {View} from '../content/model';
import {NavList} from '../../pages/control/control/control.component';
@Component({ @Component({
selector: 'elements-guacamole', selector: 'elements-guacamole',
...@@ -12,6 +11,7 @@ import {NavList} from '../../pages/control/control/control.component'; ...@@ -12,6 +11,7 @@ import {NavList} from '../../pages/control/control/control.component';
styleUrls: ['./guacamole.component.scss'] styleUrls: ['./guacamole.component.scss']
}) })
export class ElementGuacamoleComponent implements OnInit { export class ElementGuacamoleComponent implements OnInit {
@Input() view: View;
@Input() host: any; @Input() host: any;
@Input() sysUser: any; @Input() sysUser: any;
@Input() remoteAppId: string; @Input() remoteAppId: string;
...@@ -36,10 +36,7 @@ export class ElementGuacamoleComponent implements OnInit { ...@@ -36,10 +36,7 @@ export class ElementGuacamoleComponent implements OnInit {
action.subscribe( action.subscribe(
data => { data => {
const base = data.result; const base = data.result;
this.target = document.location.origin + '/guacamole/#/client/' + base + '?token=' + DataStore.guacamole_token; this.target = document.location.origin + '/guacamole/#/client/' + base + '?token=' + DataStore.guacamoleToken;
setTimeout(() => {
NavList.List[this.index].Rdp = this.el.nativeElement;
}, 500);
}, },
error => { error => {
if (!this.registered) { if (!this.registered) {
...@@ -57,8 +54,8 @@ export class ElementGuacamoleComponent implements OnInit { ...@@ -57,8 +54,8 @@ export class ElementGuacamoleComponent implements OnInit {
this._http.getGuacamoleToken(User.id, '').subscribe( this._http.getGuacamoleToken(User.id, '').subscribe(
data => { data => {
// /guacamole/client will redirect to http://guacamole/#/client // /guacamole/client will redirect to http://guacamole/#/client
DataStore.guacamole_token = data['authToken']; DataStore.guacamoleToken = data['authToken'];
DataStore.guacamole_token_time = nowTime; DataStore.guacamoleTokenTime = nowTime;
this.registerHost(); this.registerHost();
}, },
error => { error => {
...@@ -70,8 +67,8 @@ export class ElementGuacamoleComponent implements OnInit { ...@@ -70,8 +67,8 @@ export class ElementGuacamoleComponent implements OnInit {
ngOnInit() { ngOnInit() {
// /guacamole/api/tokens will redirect to http://guacamole/api/tokens // /guacamole/api/tokens will redirect to http://guacamole/api/tokens
this.view.type = 'rdp';
if (this.target) { if (this.target) {
NavList.List[this.index].Rdp = this.el.nativeElement;
return null; return null;
} }
...@@ -88,7 +85,8 @@ export class ElementGuacamoleComponent implements OnInit { ...@@ -88,7 +85,8 @@ export class ElementGuacamoleComponent implements OnInit {
} }
Disconnect() { Disconnect() {
NavList.List[this.index].connected = false; // TOdo:
return;
} }
active() { active() {
......
<div class="sidebar" fxLayout="column" ngxSplit="column"> <div class="sidebar" fxLayout="column" ngxSplit="column">
<div fxflex="1 1 30px" class="tree-filter"> <div fxflex="1 1 30px" class="tree-filter">
<elements-tree-filter></elements-tree-filter> <elements-tree-filter ></elements-tree-filter>
</div> </div>
<div class="overflow ngx-scroll-overlay" fxflex="1 1 calc(90%-60px)"> <div class="overflow ngx-scroll-overlay" fxflex="1 1 calc(90%-60px)">
<elements-asset-tree [query]="q" ></elements-asset-tree> <elements-asset-tree ></elements-asset-tree>
</div> </div>
<div class="footer-version" fxflex="1 1 30px"> <div class="footer-version" fxflex="1 1 30px">
......
...@@ -43,7 +43,7 @@ export class ElementLeftBarComponent { ...@@ -43,7 +43,7 @@ export class ElementLeftBarComponent {
} }
static Hide() { static Hide() {
DataStore.leftbarshow = false; DataStore.showLeftBar = false;
DataStore.Nav.map(function (value, i) { DataStore.Nav.map(function (value, i) {
value['children'].forEach((v, key) => { value['children'].forEach((v, key) => {
if (DataStore.Nav[i]['children'][key]['id'] === 'HideLeftManager') { if (DataStore.Nav[i]['children'][key]['id'] === 'HideLeftManager') {
...@@ -59,7 +59,7 @@ export class ElementLeftBarComponent { ...@@ -59,7 +59,7 @@ export class ElementLeftBarComponent {
} }
static Show() { static Show() {
DataStore.leftbarshow = true; DataStore.showLeftBar = true;
DataStore.Nav.map(function (value, i) { DataStore.Nav.map(function (value, i) {
value['children'].forEach((v, key) => { value['children'].forEach((v, key) => {
if (DataStore.Nav[i]['children'][key]['id'] === 'ShowLeftManager') { if (DataStore.Nav[i]['children'][key]['id'] === 'ShowLeftManager') {
......
import {Injectable, EventEmitter} from '@angular/core';
@Injectable()
export class TreeFilterService {
onFilter: EventEmitter<string> = new EventEmitter<string>();
filter(q: string) {
if (q) {
console.log('Emit key to filter...: ', q);
this.onFilter.emit(q);
}
}
}
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
* @author liuzheng <liuzheng712@gmail.com> * @author liuzheng <liuzheng712@gmail.com>
*/ */
import {Component, Inject, OnInit} from '@angular/core'; import {Component, Inject, OnInit} from '@angular/core';
import {AppService, HttpService, LocalStorageService, NavService, LogService} from '../../app.service'; import {HttpService, LocalStorageService, NavService, LogService} from '../../app.service';
import {CleftbarComponent} from '../../pages/control/cleftbar/cleftbar.component'; import {CleftbarComponent} from '../../pages/control/cleftbar/cleftbar.component';
import {ControlComponent, NavList, View} from '../../pages/control/control/control.component'; import {ControlComponent, NavList, View} from '../../pages/control/control/control.component';
import {DataStore, i18n} from '../../globals'; import {DataStore, i18n} from '../../globals';
...@@ -29,8 +29,7 @@ export class ElementNavComponent implements OnInit { ...@@ -29,8 +29,7 @@ export class ElementNavComponent implements OnInit {
jQuery('elements-nav').hide(); jQuery('elements-nav').hide();
} }
constructor(private _appService: AppService, constructor(private _http: HttpService,
private _http: HttpService,
private _logger: LogService, private _logger: LogService,
public _dialog: MatDialog, public _dialog: MatDialog,
public _navSvc: NavService, public _navSvc: NavService,
......
...@@ -12,7 +12,7 @@ import {getWsSocket} from '../../globals'; ...@@ -12,7 +12,7 @@ import {getWsSocket} from '../../globals';
templateUrl: './ssh-term.component.html', templateUrl: './ssh-term.component.html',
styleUrls: ['./ssh-term.component.scss'] styleUrls: ['./ssh-term.component.scss']
}) })
export class ElementSshTermComponent implements OnInit, AfterViewInit, OnDestroy { export class ElementSshTermComponent implements OnInit, OnDestroy {
@Input() host: any; @Input() host: any;
@Input() view: View; @Input() view: View;
@Input() sysUser: any; @Input() sysUser: any;
...@@ -34,9 +34,7 @@ export class ElementSshTermComponent implements OnInit, AfterViewInit, OnDestroy ...@@ -34,9 +34,7 @@ export class ElementSshTermComponent implements OnInit, AfterViewInit, OnDestroy
this.ws = sock; this.ws = sock;
this.connectHost(); this.connectHost();
}); });
} this.view.type = 'ssh';
ngAfterViewInit() {
} }
newTerm() { newTerm() {
......
<input #keyword id="search" class="search" <input id="search" class="search"
[formControl]="searchControl"
placeholder=" {{'Search'| trans }} ..." placeholder=" {{'Search'| trans }} ..."
maxlength="2048" maxlength="2048"
name="q" name="keyword"
autocomplete="off" autocomplete="off"
title="Search" title="Search"
type="text" tabindex="1" spellcheck="false" [(ngModel)]="q" type="text" tabindex="1" spellcheck="false"
> >
import {Component, OnChanges, Input, Pipe, PipeTransform} from '@angular/core'; import {Component, OnInit, Output, Pipe, PipeTransform, EventEmitter} from '@angular/core';
import {AppService, HttpService, LogService} from '../../app.service'; import {FormControl} from '@angular/forms';
import {debounceTime, distinctUntilChanged} from 'rxjs/operators';
import {LogService, TreeFilterService} from '../../app.service';
@Component({ @Component({
selector: 'elements-tree-filter', selector: 'elements-tree-filter',
templateUrl: './tree-filter.component.html', templateUrl: './tree-filter.component.html',
styleUrls: ['./tree-filter.component.css'] styleUrls: ['./tree-filter.component.css'],
}) })
export class ElementTreeFilterComponent implements OnChanges { export class ElementTreeFilterComponent implements OnInit {
q: string; private searchControl: FormControl;
@Input() input; private debounce = 400;
searchRequest: any;
constructor(private _appService: AppService, constructor(private _treeFilterService: TreeFilterService,
private _http: HttpService,
private _logger: LogService) { private _logger: LogService) {
this._logger.log('LeftbarComponent.ts:SearchBar');
}
ngOnChanges(changes) {
this.q = changes.input.currentValue;
}
modelChange($event) {
this.Search(this.q);
} }
public Search(q) { ngOnInit(): void {
if (this.searchRequest) { this.searchControl = new FormControl('');
this.searchRequest.unsubscribe(); this.searchControl.valueChanges
} .pipe(debounceTime(this.debounce), distinctUntilChanged())
this.searchRequest = this._http.search(q) .subscribe(query => {
.subscribe( this._logger.debug('Tree filter: ', query);
data => { this._treeFilterService.filter(query);
this._logger.log(data); });
},
err => {
this._logger.error(err);
},
() => {
}
);
this._logger.log(q);
} }
} }
......
...@@ -25,11 +25,11 @@ export const DataStore: _DataStore = { ...@@ -25,11 +25,11 @@ export const DataStore: _DataStore = {
error: {}, error: {},
msg: {}, msg: {},
loglevel: 0, loglevel: 0,
leftbarshow: true, showLeftBar: true,
windowsize: [], windowsize: [],
autologin: false, autologin: false,
guacamole_token: '', guacamoleToken: '',
guacamole_token_time: 0 guacamoleTokenTime: 0
}; };
export let Browser = new _Browser(); export let Browser = new _Browser();
......
...@@ -88,11 +88,11 @@ export class DataStore { ...@@ -88,11 +88,11 @@ export class DataStore {
error: {}; error: {};
msg: {}; msg: {};
loglevel: number; loglevel: number;
leftbarshow = true; showLeftBar = true;
windowsize: Array<number>; windowsize: Array<number>;
autologin: boolean; autologin: boolean;
guacamole_token: string; guacamoleToken: string;
guacamole_token_time: number; guacamoleTokenTime: number;
} }
......
<ng-progress></ng-progress> <ng-progress></ng-progress>
<elements-nav *ngIf="DataStore.NavShow"></elements-nav>
<router-outlet></router-outlet> <router-outlet></router-outlet>
<!--<elements-interactive></elements-interactive>--> <!--<elements-interactive></elements-interactive>-->
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
}));
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
}));
});
/** import {Component} from '@angular/core';
* 控制主页
*
*
* @date 2017-11-07
* @author liuzheng <liuzheng712@gmail.com>
*/
import {Component, HostListener} from '@angular/core';
import {DataStore} from '../globals'; import {DataStore} from '../globals';
import { environment } from '../../environments/environment'; import {AppService} from '../app.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
...@@ -18,16 +11,7 @@ import { environment } from '../../environments/environment'; ...@@ -18,16 +11,7 @@ import { environment } from '../../environments/environment';
export class AppComponent { export class AppComponent {
DataStore = DataStore; DataStore = DataStore;
constructor() {} constructor(appSrv: AppService) {
@HostListener('window:beforeunload', ['$event'])
unloadNotification($event: any) {
const notInIframe = window.self === window.top;
const notInReplay = location.pathname.indexOf('/luna/replay') === -1;
if (environment.production && notInIframe && notInReplay) {
return false;
}
return true;
} }
} }
...@@ -55,7 +55,7 @@ export class CleftbarComponent { ...@@ -55,7 +55,7 @@ export class CleftbarComponent {
} }
static Hide() { static Hide() {
DataStore.leftbarshow = false; DataStore.showLeftBar = false;
DataStore.Nav.map(function (value, i) { DataStore.Nav.map(function (value, i) {
value['children'].forEach((v, key) => { value['children'].forEach((v, key) => {
if (DataStore.Nav[i]['children'][key]['id'] === 'HideLeftManager') { if (DataStore.Nav[i]['children'][key]['id'] === 'HideLeftManager') {
...@@ -71,7 +71,7 @@ export class CleftbarComponent { ...@@ -71,7 +71,7 @@ export class CleftbarComponent {
} }
static Show() { static Show() {
DataStore.leftbarshow = true; DataStore.showLeftBar = true;
DataStore.Nav.map(function (value, i) { DataStore.Nav.map(function (value, i) {
value['children'].forEach((v, key) => { value['children'].forEach((v, key) => {
if (DataStore.Nav[i]['children'][key]['id'] === 'ShowLeftManager') { if (DataStore.Nav[i]['children'][key]['id'] === 'ShowLeftManager') {
......
<elements-nav></elements-nav> <elements-nav></elements-nav>
<div id="container" class="container-fluid row" fxLayout="row" ngxSplit="row"> <div id="container" class="container-fluid row" fxLayout="row" ngxSplit="row">
<div fxFlex="1 1 20%" minBasis="100px" maxBasis="800px" fxFlexFill ngxSplitArea *ngIf="DataStore.leftbarshow"> <div fxFlex="1 1 20%" minBasis="100px" maxBasis="800px" fxFlexFill ngxSplitArea *ngIf="DataStore.showLeftBar">
<elements-left-bar></elements-left-bar> <elements-left-bar></elements-left-bar>
</div> </div>
<div fxFlex="0" ngxSplitHandle [style.display]="activeViewIsRdp() ? 'none' : 'block'" (mouseup)="dragSplitBtn($event)"></div> <div fxFlex="0" ngxSplitHandle (mouseup)="dragSplitBtn($event)"></div>
<div [fxFlex]="DataStore.leftbarshow ? '1 1 80%' : '1 1 100%'" ngxSplitArea class="content"> <div [fxFlex]="DataStore.showLeftBar ? '1 1 80%' : '1 1 100%'" ngxSplitArea class="content">
<elements-content></elements-content> <elements-content></elements-content>
</div> </div>
</div> </div>
import {Component, OnInit} from '@angular/core'; import {Component, HostListener, OnInit} from '@angular/core';
import {DataStore, User} from '../../globals'; import {DataStore, User} from '../../globals';
import {NavList} from '../control/control/control.component'; import {environment} from '../../../environments/environment';
@Component({ @Component({
selector: 'pages-main', selector: 'pages-main',
...@@ -14,11 +14,14 @@ export class PageMainComponent implements OnInit { ...@@ -14,11 +14,14 @@ export class PageMainComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
} }
activeViewIsRdp() {
return NavList.List[NavList.Active].type === 'rdp';
}
dragSplitBtn(evt) { dragSplitBtn(evt) {
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
} }
@HostListener('window:beforeunload', ['$event'])
unloadNotification($event: any) {
const notInIframe = window.self === window.top;
const notInReplay = location.pathname.indexOf('/luna/replay') === -1;
return !(environment.production && notInIframe && notInReplay);
}
} }
import {PageMainComponent} from './main/main.component'; import {PageMainComponent} from './main/main.component';
import {PagesBlankComponent} from './blank/blank.component'; import {PagesBlankComponent} from './blank/blank.component';
import {PagesConnectComponent} from './connect/connect.component'; import {PagesConnectComponent} from './connect/connect.component';
// import {PagesControlComponent} from './control/control.component';
import {PagesMonitorComponent} from './monitor/monitor.component'; import {PagesMonitorComponent} from './monitor/monitor.component';
import {PagesReplayComponent} from './replay/replay.component'; import {PagesReplayComponent} from './replay/replay.component';
// import {PagesSettingComponent} from './setting/setting.component';
import {PagesNotFoundComponent} from './not-found/not-found.component'; import {PagesNotFoundComponent} from './not-found/not-found.component';
import {PagesLoginComponent} from './login/login.component'; import {PagesLoginComponent} from './login/login.component';
// import {CleftbarComponent} from './control/cleftbar/cleftbar.component';
import {JsonComponent} from './replay/json/json.component'; import {JsonComponent} from './replay/json/json.component';
// import {ControlComponent} from './control/control/control.component';
// import {PagesControlNavComponent} from './control/control/controlnav/nav.component';
// import {SearchComponent, SearchFilter} from './control/search/search.component';
import {PagesMonitorLinuxComponent} from './monitor/linux/linux.component'; import {PagesMonitorLinuxComponent} from './monitor/linux/linux.component';
import {PagesMonitorWindowsComponent} from './monitor/windows/windows.component'; import {PagesMonitorWindowsComponent} from './monitor/windows/windows.component';
import {ReplayGuacamoleComponent} from './replay/guacamole/guacamole.component'; import {ReplayGuacamoleComponent} from './replay/guacamole/guacamole.component';
...@@ -20,15 +14,10 @@ export const PagesComponents = [ ...@@ -20,15 +14,10 @@ export const PagesComponents = [
PageMainComponent, PageMainComponent,
PagesBlankComponent, PagesBlankComponent,
PagesConnectComponent, PagesConnectComponent,
// PagesControlComponent, ControlComponent, PagesControlNavComponent,
// CleftbarComponent,
PagesMonitorComponent, PagesMonitorComponent,
PagesReplayComponent, JsonComponent, PagesReplayComponent, JsonComponent,
// PagesSettingComponent,
PagesNotFoundComponent, PagesNotFoundComponent,
PagesLoginComponent, PagesLoginComponent,
// SearchComponent,
// SearchFilter,
PagesMonitorLinuxComponent, PagesMonitorLinuxComponent,
PagesMonitorWindowsComponent, PagesMonitorWindowsComponent,
ReplayGuacamoleComponent ReplayGuacamoleComponent
......
...@@ -9,7 +9,7 @@ export const PluginModules = [ ...@@ -9,7 +9,7 @@ export const PluginModules = [
BrowserAnimationsModule, BrowserAnimationsModule,
NgProgressModule, NgProgressModule,
MaterialModule, MaterialModule,
LoggerModule.forRoot({serverLoggingUrl: '/api/logs', level: NgxLoggerLevel.DEBUG, serverLogLevel: NgxLoggerLevel.ERROR}), LoggerModule.forRoot({level: NgxLoggerLevel.DEBUG, serverLogLevel: NgxLoggerLevel.ERROR}),
NgxDatatableModule, NgxDatatableModule,
NgxUIModule, NgxUIModule,
SplitModule SplitModule
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment